Skip to content

Commit a72b093

Browse files
committed
refactor(linter/plugins): enable TS strictNullChecks (#15886)
Adapted from #15868. Enable `strictNullChecks` in TSConfig for `apps/oxlint`. Rather than using `!` all over the place, I've added a function `assertIsNonNull`, which performs the same function. Idea is that we can later on produce a debug build where `assertIsNonNull` does an actual runtime assertion (rather than just a TS `asserts`) that the value is not `null` or `undefined`. We can use the debug build in tests, and catch any logic errors. Unfortunately, TypeScript doesn't seem able to track side effects, so there seems to be no way to tell it "after calling `initAst()`, `ast` is always non-null". Don't want to switch to a having a `getAst` function which returns a non-null value, as that'd introduce unnecessary temp vars, and function calls, on hot paths. So this seems to be the best we can do without incurring runtime cost.
1 parent a299a38 commit a72b093

File tree

23 files changed

+131
-70
lines changed

23 files changed

+131
-70
lines changed

apps/oxlint/src-js/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function loadPluginWrapper(path: string, packageName: string | null): Promise<st
2424
return loadPlugin(path, packageName);
2525
});
2626
}
27-
return loadPlugin(path, packageName);
27+
return loadPlugin!(path, packageName);
2828
}
2929

3030
/**

apps/oxlint/src-js/index.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { assertIsNonNull } from './plugins/utils.js';
2+
13
import type { Context, FileContext, LanguageOptions } from './plugins/context.ts';
24
import type { CreateOnceRule, Plugin, Rule } from './plugins/load.ts';
35
import type { Settings } from './plugins/settings.ts';
46
import type { SourceCode } from './plugins/source_code.ts';
57
import type { BeforeHook, Visitor, VisitorWithHooks } from './plugins/types.ts';
8+
import type { SetNullable } from './plugins/utils.ts';
69

710
export type * as ESTree from './generated/types.d.ts';
811
export type { Context, LanguageOptions } from './plugins/context.ts';
@@ -114,6 +117,7 @@ export function defineRule(rule: Rule): Rule {
114117
if (context === null) {
115118
({ context, visitor, beforeHook } = createContextAndVisitor(rule));
116119
}
120+
assertIsNonNull(visitor);
117121

118122
// Copy properties from ESLint's context object to `context`.
119123
// ESLint's context object is an object of form `{ id, options, report }`, with all other properties
@@ -131,7 +135,7 @@ export function defineRule(rule: Rule): Rule {
131135
}
132136

133137
// Return same visitor each time
134-
return visitor!;
138+
return visitor;
135139
};
136140

137141
return rule;
@@ -231,9 +235,13 @@ function createContextAndVisitor(rule: CreateOnceRule): {
231235
report: { value: null, enumerable: true, configurable: true },
232236
});
233237

234-
let { before: beforeHook, after: afterHook, ...visitor } = createOnce.call(rule, context) as VisitorWithHooks;
238+
let {
239+
before: beforeHook,
240+
after: afterHook,
241+
...visitor
242+
} = createOnce.call(rule, context) as SetNullable<VisitorWithHooks, 'before' | 'after'>;
235243

236-
if (beforeHook === void 0) {
244+
if (beforeHook === undefined) {
237245
beforeHook = null;
238246
} else if (beforeHook !== null && typeof beforeHook !== 'function') {
239247
throw new Error('`before` property of visitor must be a function if defined');

apps/oxlint/src-js/plugins/comments.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import { ast, initAst, sourceText } from './source_code.js';
6+
import { assertIsNonNull } from './utils.js';
67

78
import type { Comment, Node, NodeOrToken } from './types.ts';
89

@@ -15,6 +16,8 @@ const WHITESPACE_ONLY_REGEXP = /^\s*$/;
1516
*/
1617
export function getAllComments(): Comment[] {
1718
if (ast === null) initAst();
19+
assertIsNonNull(ast);
20+
1821
// `comments` property is a getter. Comments are deserialized lazily.
1922
return ast.comments;
2023
}
@@ -38,6 +41,8 @@ export function getAllComments(): Comment[] {
3841
*/
3942
export function getCommentsBefore(nodeOrToken: NodeOrToken): Comment[] {
4043
if (ast === null) initAst();
44+
assertIsNonNull(ast);
45+
assertIsNonNull(sourceText);
4146

4247
const { comments } = ast,
4348
commentsLength = comments.length;
@@ -93,6 +98,8 @@ export function getCommentsBefore(nodeOrToken: NodeOrToken): Comment[] {
9398
*/
9499
export function getCommentsAfter(nodeOrToken: NodeOrToken): Comment[] {
95100
if (ast === null) initAst();
101+
assertIsNonNull(ast);
102+
assertIsNonNull(sourceText);
96103

97104
const { comments } = ast,
98105
commentsLength = comments.length;
@@ -135,6 +142,7 @@ export function getCommentsAfter(nodeOrToken: NodeOrToken): Comment[] {
135142
*/
136143
export function getCommentsInside(node: Node): Comment[] {
137144
if (ast === null) initAst();
145+
assertIsNonNull(ast);
138146

139147
const { comments } = ast,
140148
commentsLength = comments.length;
@@ -178,6 +186,7 @@ export function getCommentsInside(node: Node): Comment[] {
178186
*/
179187
export function commentsExistBetween(nodeOrToken1: NodeOrToken, nodeOrToken2: NodeOrToken): boolean {
180188
if (ast === null) initAst();
189+
assertIsNonNull(ast);
181190

182191
// Find the first comment after `nodeOrToken1` ends.
183192
const { comments } = ast,

apps/oxlint/src-js/plugins/context.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import { ast, initAst, SOURCE_CODE } from './source_code.js';
3030
import { report } from './report.js';
3131
import { settings, initSettings } from './settings.js';
32+
import { assertIsNonNull } from './utils.js';
3233

3334
import type { Options, RuleDetails } from './load.ts';
3435
import type { Diagnostic } from './report.ts';
@@ -78,6 +79,8 @@ const PARSER_OPTIONS = freeze({
7879
// in case it's used in `create` to return an empty visitor if wrong type.
7980
// TODO: ESLint also has `commonjs` option.
8081
if (ast === null) initAst();
82+
assertIsNonNull(ast);
83+
8184
return ast.sourceType;
8285
},
8386
});
@@ -92,6 +95,8 @@ const LANGUAGE_OPTIONS = freeze({
9295
// in case it's used in `create` to return an empty visitor if wrong type.
9396
// TODO: ESLint also has `commonjs` option.
9497
if (ast === null) initAst();
98+
assertIsNonNull(ast);
99+
95100
return ast.sourceType;
96101
},
97102

@@ -250,7 +255,10 @@ const FILE_CONTEXT = freeze({
250255
*/
251256
get settings(): Readonly<Settings> {
252257
if (filePath === null) throw new Error('Cannot access `context.settings` in `createOnce`');
258+
253259
if (settings === null) initSettings();
260+
assertIsNonNull(settings);
261+
254262
return settings;
255263
},
256264

apps/oxlint/src-js/plugins/lint.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { registeredRules } from './load.js';
33
import { diagnostics } from './report.js';
44
import { setSettingsForFile, resetSettings } from './settings.js';
55
import { ast, initAst, resetSourceAndAst, setupSourceForFile } from './source_code.js';
6-
import { assertIs, getErrorMessage } from './utils.js';
6+
import { assertIs, assertIsNonNull, getErrorMessage } from './utils.js';
77
import { addVisitorToCompiled, compiledVisitor, finalizeCompiledVisitor, initCompiledVisitor } from './visitor.js';
88

99
// Lazy implementation
@@ -136,6 +136,7 @@ function lintFileImpl(
136136
let { visitor } = ruleDetails;
137137
if (visitor === null) {
138138
// Rule defined with `create` method
139+
assertIsNonNull(ruleDetails.rule.create);
139140
visitor = ruleDetails.rule.create(ruleDetails.context);
140141
} else {
141142
// Rule defined with `createOnce` method

apps/oxlint/src-js/plugins/load.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { Context } from './context.ts';
88
import type { JsonValue } from './json.ts';
99
import type { RuleMeta } from './rule_meta.ts';
1010
import type { AfterHook, BeforeHook, Visitor, VisitorWithHooks } from './types.ts';
11+
import type { SetNullable } from './utils.ts';
1112

1213
const ObjectKeys = Object.keys;
1314

@@ -181,7 +182,7 @@ async function loadPluginImpl(path: string, packageName: string | null): Promise
181182
// Create `RuleDetails` object for rule.
182183
const ruleDetails: RuleDetails = {
183184
rule: rule as CreateRule, // Could also be `CreateOnceRule`, but just to satisfy type checker
184-
context: null as Readonly<Context>, // Filled in below
185+
context: null!, // Filled in below
185186
isFixable,
186187
messages,
187188
ruleIndex: 0,
@@ -197,7 +198,7 @@ async function loadPluginImpl(path: string, packageName: string | null): Promise
197198

198199
if ('createOnce' in rule) {
199200
// TODO: Compile visitor object to array here, instead of repeating compilation on each file
200-
let visitorWithHooks = rule.createOnce(context);
201+
let visitorWithHooks = rule.createOnce(context) as SetNullable<VisitorWithHooks, 'before' | 'after'>;
201202
if (typeof visitorWithHooks !== 'object' || visitorWithHooks === null) {
202203
throw new TypeError('`createOnce` must return an object');
203204
}
@@ -219,9 +220,9 @@ async function loadPluginImpl(path: string, packageName: string | null): Promise
219220
afterHook = null;
220221
}
221222

222-
(ruleDetails as Writable<CreateOnceRuleDetails>).visitor = visitor;
223-
(ruleDetails as Writable<CreateOnceRuleDetails>).beforeHook = beforeHook;
224-
(ruleDetails as Writable<CreateOnceRuleDetails>).afterHook = afterHook;
223+
(ruleDetails as unknown as Writable<CreateOnceRuleDetails>).visitor = visitor;
224+
(ruleDetails as unknown as Writable<CreateOnceRuleDetails>).beforeHook = beforeHook;
225+
(ruleDetails as unknown as Writable<CreateOnceRuleDetails>).afterHook = afterHook;
225226
}
226227

227228
registeredRules.push(ruleDetails);

apps/oxlint/src-js/plugins/location.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { initSourceText, sourceText } from './source_code.js';
7+
import { assertIsNonNull } from './utils.js';
78

89
import type { Node } from './types.ts';
910

@@ -60,6 +61,7 @@ const lineStartOffsets: number[] = [0];
6061
*/
6162
export function initLines(): void {
6263
if (sourceText === null) initSourceText();
64+
assertIsNonNull(sourceText);
6365

6466
// This implementation is based on the one in ESLint.
6567
// TODO: Investigate if using `String.prototype.matchAll` is faster.
@@ -109,6 +111,7 @@ export function getLineColumnFromOffset(offset: number): LineColumn {
109111
// Build `lines` and `lineStartOffsets` tables if they haven't been already.
110112
// This also decodes `sourceText` if it wasn't already.
111113
if (lines.length === 0) initLines();
114+
assertIsNonNull(sourceText);
112115

113116
if (offset > sourceText.length) {
114117
throw new RangeError(
@@ -158,6 +161,7 @@ export function getOffsetFromLineColumn(loc: LineColumn): number {
158161
// Build `lines` and `lineStartOffsets` tables if they haven't been already.
159162
// This also decodes `sourceText` if it wasn't already.
160163
if (lines.length === 0) initLines();
164+
assertIsNonNull(sourceText);
161165

162166
const linesCount = lineStartOffsets.length;
163167
if (line <= 0 || line > linesCount) {

apps/oxlint/src-js/plugins/report.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export function report(diagnostic: Diagnostic, ruleDetails: RuleDetails): void {
8484
}
8585

8686
// TODO: Validate `diagnostic`
87-
let start: number, end: number, loc: Location;
87+
let start: number, end: number, loc: Location | undefined;
8888

8989
if (hasOwn(diagnostic, 'loc') && (loc = diagnostic.loc) != null) {
9090
// `loc`

apps/oxlint/src-js/plugins/scope.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
type ScopeManager as TSESLintScopeManager,
99
} from '@typescript-eslint/scope-manager';
1010
import { ast, initAst } from './source_code.js';
11-
import { assertIs } from './utils.js';
11+
import { assertIs, assertIsNonNull } from './utils.js';
1212

1313
import type * as ESTree from '../generated/types.d.ts';
1414
import type { SetNullable } from './utils.ts';
@@ -109,6 +109,7 @@ const analyzeOptions: SetNullable<AnalyzeOptions, 'sourceType'> = {
109109
*/
110110
function initTsScopeManager() {
111111
if (ast === null) initAst();
112+
assertIsNonNull(ast);
112113

113114
analyzeOptions.sourceType = ast.sourceType;
114115
assertIs<AnalyzeOptions>(analyzeOptions);
@@ -199,6 +200,7 @@ export function isGlobalReference(node: ESTree.Node): boolean {
199200
if (node.type !== 'Identifier') return false;
200201

201202
if (tsScopeManager === null) initTsScopeManager();
203+
assertIsNonNull(tsScopeManager);
202204

203205
const { scopes } = tsScopeManager;
204206
if (scopes.length === 0) return false;
@@ -229,6 +231,8 @@ export function isGlobalReference(node: ESTree.Node): boolean {
229231
export function getDeclaredVariables(node: ESTree.Node): Variable[] {
230232
// ref: https://github.com/eslint/eslint/blob/e7cda3bdf1bdd664e6033503a3315ad81736b200/lib/languages/js/source-code/source-code.js#L904
231233
if (tsScopeManager === null) initTsScopeManager();
234+
assertIsNonNull(tsScopeManager);
235+
232236
// @ts-expect-error // TODO: Our types don't quite align yet
233237
return tsScopeManager.getDeclaredVariables(node);
234238
}
@@ -243,6 +247,7 @@ export function getScope(node: ESTree.Node): Scope {
243247
if (!node) throw new TypeError('Missing required argument: `node`');
244248

245249
if (tsScopeManager === null) initTsScopeManager();
250+
assertIsNonNull(tsScopeManager);
246251

247252
const inner = node.type !== 'Program';
248253

@@ -254,6 +259,7 @@ export function getScope(node: ESTree.Node): Scope {
254259
return scope.type === 'function-expression-name' ? scope.childScopes[0] : scope;
255260
}
256261

262+
// @ts-expect-error - Don't want to create a new variable just to make it nullable
257263
node = node.parent;
258264
} while (node !== null);
259265

apps/oxlint/src-js/plugins/selector.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const EMPTY_TYPE_IDS_ARRAY: NodeTypeId[] = [];
5454
export function parseSelector(key: string): Selector {
5555
// Used cached object if we've parsed this key before
5656
let selector = cache.get(key);
57-
if (selector !== void 0) return selector;
57+
if (selector !== undefined) return selector;
5858

5959
// Parse with `esquery` and analyse
6060
const esquerySelector = esqueryParse(key);
@@ -97,7 +97,7 @@ function analyzeSelector(esquerySelector: EsquerySelector, selector: Selector):
9797
// If the type is invalid, just treat this selector as not matching any types.
9898
// But still increment `identifierCount`.
9999
// This matches ESLint's behavior.
100-
return typeId === void 0 ? EMPTY_TYPE_IDS_ARRAY : [typeId];
100+
return typeId === undefined ? EMPTY_TYPE_IDS_ARRAY : [typeId];
101101
}
102102

103103
case 'not':
@@ -144,7 +144,7 @@ function analyzeSelector(esquerySelector: EsquerySelector, selector: Selector):
144144
} else {
145145
// Selector only matches intersection of all child selectors.
146146
// TODO: Could make this faster if `analyzeSelector` always returned an ordered array.
147-
nodeTypes = childNodeTypes.filter((nodeType) => nodeTypes.includes(nodeType));
147+
nodeTypes = childNodeTypes.filter((nodeType) => nodeTypes!.includes(nodeType));
148148
}
149149
}
150150
return nodeTypes;

0 commit comments

Comments
 (0)