@jameswyse/oxc-config 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -186
- package/REFERENCE.md +105 -132
- package/dist/index.js +26 -0
- package/dist/lint.d.ts +1 -0
- package/dist/lint.js +124 -4
- package/dist/rules/explicit-api-states.js +7 -1
- package/dist/rules/import-boundaries.js +31 -6
- package/dist/rules/no-derived-test-expectation.d.ts +1 -0
- package/dist/rules/no-derived-test-expectation.js +136 -0
- package/dist/rules/no-disabled-tls-verification.d.ts +1 -0
- package/dist/rules/no-disabled-tls-verification.js +42 -0
- package/dist/rules/no-fixed-test-delay.js +46 -5
- package/dist/rules/no-generic-object-guard.d.ts +1 -0
- package/dist/rules/no-generic-object-guard.js +279 -0
- package/dist/rules/no-get-password-form.d.ts +1 -0
- package/dist/rules/no-get-password-form.js +113 -0
- package/dist/rules/no-hardcoded-credential.d.ts +1 -0
- package/dist/rules/no-hardcoded-credential.js +45 -0
- package/dist/rules/no-import-time-effects.d.ts +1 -0
- package/dist/rules/no-import-time-effects.js +90 -0
- package/dist/rules/no-optional-test-action.d.ts +1 -0
- package/dist/rules/no-optional-test-action.js +45 -0
- package/dist/rules/no-secret-fallback.d.ts +1 -0
- package/dist/rules/no-secret-fallback.js +64 -0
- package/dist/rules/no-swallowed-errors.js +131 -16
- package/dist/rules/no-test-timeout-overrides.js +22 -9
- package/dist/rules/no-unchecked-fetch-response.d.ts +1 -0
- package/dist/rules/no-unchecked-fetch-response.js +91 -0
- package/dist/rules/no-unchecked-index-search.d.ts +1 -0
- package/dist/rules/no-unchecked-index-search.js +41 -0
- package/dist/rules/no-uncontrolled-test-clock.d.ts +1 -0
- package/dist/rules/no-uncontrolled-test-clock.js +112 -0
- package/dist/rules/no-unsettled-promise.d.ts +1 -0
- package/dist/rules/no-unsettled-promise.js +64 -0
- package/dist/rules/require-suppression-reason.d.ts +1 -0
- package/dist/rules/require-suppression-reason.js +32 -0
- package/dist/shared/index.d.ts +9 -0
- package/dist/shared/index.js +70 -0
- package/dist/vendor/anti-slop/effect/rules/no-service-constructor-imports.js +10 -2
- package/dist/vendor/anti-slop/rules/no-known-value-widening.js +1 -1
- package/dist/vendor/anti-slop/rules/no-module-mocking.js +56 -1
- package/dist/vendor/anti-slop/rules/no-runtime-typeof.js +30 -1
- package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.js +28 -1
- package/dist/vendor/anti-slop/rules/no-unknown-returns.js +1 -1
- package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.js +2 -2
- package/dist/vendor/anti-slop/shared/dictionary-types.js +72 -89
- package/dist/vendor/anti-slop/shared/type-alias-resolution.d.ts +8 -2
- package/dist/vendor/anti-slop/shared/type-alias-resolution.js +2 -2
- package/package.json +7 -7
package/dist/shared/index.d.ts
CHANGED
|
@@ -23,4 +23,13 @@ export declare function projectPath(cwd: string, file: string): string;
|
|
|
23
23
|
export declare function matchesAny(path: string, patterns?: readonly string[]): boolean;
|
|
24
24
|
export declare function isGlobalValue(sourceCode: SourceCode, node: ESTree.Node | null | undefined, name: string): boolean;
|
|
25
25
|
export declare function isInjectionSeam(source: ESTree.Node): boolean;
|
|
26
|
+
export declare function isShortCircuited(call: ESTree.CallExpression): boolean;
|
|
27
|
+
export declare function staticPropertyName(node: ESTree.Node): string | null;
|
|
28
|
+
export declare function importedBinding(sourceCode: SourceCode, node: ESTree.Node): {
|
|
29
|
+
readonly source: string;
|
|
30
|
+
readonly name: string;
|
|
31
|
+
} | null;
|
|
32
|
+
export declare function isEffectModule(sourceCode: SourceCode, node: ESTree.Node, module: string): boolean;
|
|
33
|
+
export declare function unwrapExpression(node: ESTree.Node): ESTree.Node;
|
|
34
|
+
export declare function outermostExpression(node: ESTree.Node): ESTree.Node;
|
|
26
35
|
export {};
|
package/dist/shared/index.js
CHANGED
|
@@ -258,3 +258,73 @@ export function isInjectionSeam(source) {
|
|
|
258
258
|
}
|
|
259
259
|
return false;
|
|
260
260
|
}
|
|
261
|
+
export function isShortCircuited(call) {
|
|
262
|
+
let current = call;
|
|
263
|
+
while (current.type === "CallExpression" || current.type === "MemberExpression") {
|
|
264
|
+
if (current.optional) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
current = current.type === "CallExpression" ? current.callee : current.object;
|
|
268
|
+
while (current.type === "TSNonNullExpression") {
|
|
269
|
+
current = current.expression;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
export function staticPropertyName(node) {
|
|
275
|
+
if (!("computed" in node && node.computed)) {
|
|
276
|
+
return propertyName(node);
|
|
277
|
+
}
|
|
278
|
+
const key = memberKey(node);
|
|
279
|
+
return key?.type === "Literal" && typeof key.value === "string" ? key.value : null;
|
|
280
|
+
}
|
|
281
|
+
export function importedBinding(sourceCode, node) {
|
|
282
|
+
if (node.type === "MemberExpression") {
|
|
283
|
+
const owner = importedBinding(sourceCode, node.object);
|
|
284
|
+
const name = staticPropertyName(node);
|
|
285
|
+
return (owner?.name === "*" || owner?.name === "default") && name !== null
|
|
286
|
+
? { source: owner.source, name }
|
|
287
|
+
: null;
|
|
288
|
+
}
|
|
289
|
+
const definition = variableFor(sourceCode, node)?.defs[0];
|
|
290
|
+
const declaration = definition?.parent;
|
|
291
|
+
if (definition?.type !== "ImportBinding" || declaration?.type !== "ImportDeclaration") {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const specifier = definition.node;
|
|
295
|
+
const source = declaration.source.value;
|
|
296
|
+
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
297
|
+
return { source, name: "*" };
|
|
298
|
+
}
|
|
299
|
+
if (specifier.type === "ImportDefaultSpecifier") {
|
|
300
|
+
return { source, name: "default" };
|
|
301
|
+
}
|
|
302
|
+
return specifier.type === "ImportSpecifier"
|
|
303
|
+
? {
|
|
304
|
+
source,
|
|
305
|
+
name: specifier.imported.type === "Identifier"
|
|
306
|
+
? specifier.imported.name
|
|
307
|
+
: specifier.imported.value,
|
|
308
|
+
}
|
|
309
|
+
: null;
|
|
310
|
+
}
|
|
311
|
+
export function isEffectModule(sourceCode, node, module) {
|
|
312
|
+
const binding = importedBinding(sourceCode, node);
|
|
313
|
+
return ((binding?.source === "effect" && binding.name === module) ||
|
|
314
|
+
(binding?.source === `effect/${module}` && binding.name === "*"));
|
|
315
|
+
}
|
|
316
|
+
export function unwrapExpression(node) {
|
|
317
|
+
return node.type === "ParenthesizedExpression" ||
|
|
318
|
+
node.type === "TSAsExpression" ||
|
|
319
|
+
node.type === "TSSatisfiesExpression" ||
|
|
320
|
+
node.type === "TSNonNullExpression" ||
|
|
321
|
+
node.type === "TSTypeAssertion"
|
|
322
|
+
? unwrapExpression(node.expression)
|
|
323
|
+
: node;
|
|
324
|
+
}
|
|
325
|
+
export function outermostExpression(node) {
|
|
326
|
+
const parent = node.parent;
|
|
327
|
+
return parent !== null && unwrapExpression(parent) === unwrapExpression(node)
|
|
328
|
+
? outermostExpression(parent)
|
|
329
|
+
: node;
|
|
330
|
+
}
|
|
@@ -11,11 +11,16 @@ function getImportedName(specifier) {
|
|
|
11
11
|
}
|
|
12
12
|
return specifier.imported.value;
|
|
13
13
|
}
|
|
14
|
+
function isCallOrTypeQuery(reference) {
|
|
15
|
+
const parent = reference.identifier.parent;
|
|
16
|
+
return (parent.type === "TSTypeQuery" ||
|
|
17
|
+
(parent.type === "CallExpression" && parent.callee === reference.identifier));
|
|
18
|
+
}
|
|
14
19
|
export const noServiceConstructorImports = defineRule({
|
|
15
20
|
meta: {
|
|
16
21
|
type: "problem",
|
|
17
22
|
docs: {
|
|
18
|
-
description: "Disallow project-local make<CapabilityName>
|
|
23
|
+
description: "Disallow project-local make<CapabilityName> service values imported outside test and spec files.",
|
|
19
24
|
},
|
|
20
25
|
messages: {
|
|
21
26
|
serviceConstructorImport: 'Do not import Effect service constructor "{{name}}" into runtime code. Import the owning Layer, yield the contextual service, and allow its requirements to propagate to the composition root.',
|
|
@@ -33,7 +38,10 @@ export const noServiceConstructorImports = defineRule({
|
|
|
33
38
|
continue;
|
|
34
39
|
}
|
|
35
40
|
const importedName = getImportedName(specifier);
|
|
36
|
-
if (!SERVICE_CONSTRUCTOR_NAME.test(importedName)
|
|
41
|
+
if (!SERVICE_CONSTRUCTOR_NAME.test(importedName) ||
|
|
42
|
+
context.sourceCode
|
|
43
|
+
.getDeclaredVariables(specifier)
|
|
44
|
+
.every((variable) => variable.references.every(isCallOrTypeQuery))) {
|
|
37
45
|
continue;
|
|
38
46
|
}
|
|
39
47
|
context.report({
|
|
@@ -210,7 +210,7 @@ export const noKnownValueWidening = defineRule({
|
|
|
210
210
|
description: "Disallow syntactically established values from flowing into explicitly broad or anonymous target types that discard useful evidence.",
|
|
211
211
|
},
|
|
212
212
|
messages: {
|
|
213
|
-
widening: "The explicit {{target}} type on {{subject}} discards
|
|
213
|
+
widening: "The explicit {{target}} type on {{subject}} discards what the value already proves. Keep the inferred type, check it with `satisfies`, or annotate it with the named type its owner exports.",
|
|
214
214
|
},
|
|
215
215
|
},
|
|
216
216
|
createOnce(context) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Adapted from dmmulroy/anti-slop
|
|
2
2
|
import { defineRule } from "@oxlint/plugins";
|
|
3
|
+
import { apiReference, outermostExpression, propertyName } from "../../../shared/index.js";
|
|
3
4
|
import { resolveVariable } from "../shared/scope.js";
|
|
4
5
|
const moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]);
|
|
5
6
|
function importedName(node) {
|
|
@@ -26,13 +27,67 @@ function isTestFrameworkObject(sourceCode, expression) {
|
|
|
26
27
|
}
|
|
27
28
|
const source = definition.parent.source.value;
|
|
28
29
|
const name = importedName(definition.node);
|
|
29
|
-
return ((source === "vitest"
|
|
30
|
+
return (((source === "vitest" || source === "@effect/vitest") && name === "vi") ||
|
|
31
|
+
(source === "@jest/globals" && name === "jest"));
|
|
30
32
|
});
|
|
31
33
|
}
|
|
34
|
+
const nodeTestModules = ["node:test", "test"];
|
|
35
|
+
const nodeTestContextMethods = new Set(["test", "before", "after", "beforeEach", "afterEach"]);
|
|
36
|
+
function callbackCalls(sourceCode, callback) {
|
|
37
|
+
const argument = outermostExpression(callback);
|
|
38
|
+
const declarator = argument.parent;
|
|
39
|
+
const holder = callback.type === "FunctionDeclaration"
|
|
40
|
+
? callback
|
|
41
|
+
: declarator?.type === "VariableDeclarator" && declarator.init === argument
|
|
42
|
+
? declarator
|
|
43
|
+
: null;
|
|
44
|
+
const uses = holder === null
|
|
45
|
+
? [argument]
|
|
46
|
+
: sourceCode
|
|
47
|
+
.getDeclaredVariables(holder)
|
|
48
|
+
.flatMap((variable) => variable.references.map((reference) => outermostExpression(reference.identifier)));
|
|
49
|
+
return uses.flatMap((use) => use.parent?.type === "CallExpression" && use.parent.arguments.some((item) => item === use)
|
|
50
|
+
? [use.parent]
|
|
51
|
+
: []);
|
|
52
|
+
}
|
|
53
|
+
function isNodeTestContext(sourceCode, node) {
|
|
54
|
+
const definition = resolveVariable(sourceCode, node)?.defs[0];
|
|
55
|
+
const callback = definition?.type === "Parameter" ? definition.node : null;
|
|
56
|
+
if (callback === null || !("params" in callback) || callback.params[0] !== definition?.name) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return callbackCalls(sourceCode, callback).some(({ callee }) => nodeTestModules.includes(apiReference(sourceCode, callee)?.module ?? "") ||
|
|
60
|
+
(callee.type === "MemberExpression" &&
|
|
61
|
+
callee.object.type === "Identifier" &&
|
|
62
|
+
nodeTestContextMethods.has(propertyName(callee) ?? "") &&
|
|
63
|
+
isNodeTestContext(sourceCode, callee.object)));
|
|
64
|
+
}
|
|
65
|
+
function isNodeTestMock(sourceCode, expression) {
|
|
66
|
+
if (expression.type === "MemberExpression") {
|
|
67
|
+
return (!expression.computed &&
|
|
68
|
+
expression.property.type === "Identifier" &&
|
|
69
|
+
expression.property.name === "mock" &&
|
|
70
|
+
expression.object.type === "Identifier" &&
|
|
71
|
+
isNodeTestContext(sourceCode, expression.object));
|
|
72
|
+
}
|
|
73
|
+
if (expression.type !== "Identifier") {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return (resolveVariable(sourceCode, expression)?.defs.some((definition) => definition.type === "ImportBinding" &&
|
|
77
|
+
definition.parent?.type === "ImportDeclaration" &&
|
|
78
|
+
nodeTestModules.includes(definition.parent.source.value) &&
|
|
79
|
+
importedName(definition.node) === "mock") ?? false);
|
|
80
|
+
}
|
|
32
81
|
function moduleMockCall(sourceCode, callee) {
|
|
33
82
|
if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) {
|
|
34
83
|
return false;
|
|
35
84
|
}
|
|
85
|
+
if (!callee.computed &&
|
|
86
|
+
callee.property.type === "Identifier" &&
|
|
87
|
+
callee.property.name === "module" &&
|
|
88
|
+
isNodeTestMock(sourceCode, callee.object)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
36
91
|
if (!isTestFrameworkObject(sourceCode, callee.object)) {
|
|
37
92
|
return false;
|
|
38
93
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Adapted from dmmulroy/anti-slop
|
|
2
2
|
import { defineRule } from "@oxlint/plugins";
|
|
3
|
+
import { variableFor } from "../../../shared/index.js";
|
|
4
|
+
import { createTypeAliasEnvironment, resolvedTypeMatches, } from "../shared/type-alias-resolution.js";
|
|
3
5
|
function isRuntimeFunction(node) {
|
|
4
6
|
return (node.type === "ArrowFunctionExpression" ||
|
|
5
7
|
node.type === "FunctionDeclaration" ||
|
|
@@ -26,6 +28,24 @@ function isExistenceProbe(node) {
|
|
|
26
28
|
const other = parent.left === node ? parent.right : parent.left;
|
|
27
29
|
return other.type === "Literal" && other.value === "undefined";
|
|
28
30
|
}
|
|
31
|
+
function declaredType(sourceCode, node) {
|
|
32
|
+
const name = variableFor(sourceCode, node)?.defs[0]?.name;
|
|
33
|
+
return name?.type === "Identifier" ? (name.typeAnnotation?.typeAnnotation ?? null) : null;
|
|
34
|
+
}
|
|
35
|
+
function isUnion(type, environment) {
|
|
36
|
+
return resolvedTypeMatches(type, environment, (resolved, matches) => resolved.type === "TSParenthesizedType"
|
|
37
|
+
? matches(resolved.typeAnnotation)
|
|
38
|
+
: resolved.type === "TSUnionType");
|
|
39
|
+
}
|
|
40
|
+
function isOpen(type, environment) {
|
|
41
|
+
return resolvedTypeMatches(type, environment, (resolved, matches) => resolved.type === "TSUnknownKeyword" ||
|
|
42
|
+
resolved.type === "TSAnyKeyword" ||
|
|
43
|
+
(resolved.type === "TSParenthesizedType" && matches(resolved.typeAnnotation)) ||
|
|
44
|
+
(resolved.type === "TSUnionType" && resolved.types.some(matches)));
|
|
45
|
+
}
|
|
46
|
+
function isKnownUnion(type, environment) {
|
|
47
|
+
return isUnion(type, environment) && !isOpen(type, environment);
|
|
48
|
+
}
|
|
29
49
|
export const noRuntimeTypeof = defineRule({
|
|
30
50
|
meta: {
|
|
31
51
|
type: "problem",
|
|
@@ -33,7 +53,7 @@ export const noRuntimeTypeof = defineRule({
|
|
|
33
53
|
description: "Disallow runtime typeof checks; external values must be decoded into meaningful types at their I/O boundary.",
|
|
34
54
|
},
|
|
35
55
|
messages: {
|
|
36
|
-
runtimeTypeof: "A `typeof` check narrows a representation without
|
|
56
|
+
runtimeTypeof: "A `typeof` check narrows a value's representation without proving its shape. Parse input where it enters, then branch on the domain value.",
|
|
37
57
|
},
|
|
38
58
|
schema: [
|
|
39
59
|
{
|
|
@@ -45,7 +65,15 @@ export const noRuntimeTypeof = defineRule({
|
|
|
45
65
|
defaultOptions: [{ allowInTypeGuards: false }],
|
|
46
66
|
},
|
|
47
67
|
createOnce(context) {
|
|
68
|
+
let environment = null;
|
|
69
|
+
function discriminatesKnownUnion(node) {
|
|
70
|
+
const type = declaredType(context.sourceCode, node.argument);
|
|
71
|
+
return type !== null && environment !== null && isKnownUnion(type, environment);
|
|
72
|
+
}
|
|
48
73
|
return {
|
|
74
|
+
Program(program) {
|
|
75
|
+
environment = createTypeAliasEnvironment(program, context.sourceCode.visitorKeys);
|
|
76
|
+
},
|
|
49
77
|
UnaryExpression(node) {
|
|
50
78
|
const option = context.options[0];
|
|
51
79
|
const allowInTypeGuards = typeof option === "object" &&
|
|
@@ -54,6 +82,7 @@ export const noRuntimeTypeof = defineRule({
|
|
|
54
82
|
option.allowInTypeGuards === true;
|
|
55
83
|
if (node.operator === "typeof" &&
|
|
56
84
|
!isExistenceProbe(node) &&
|
|
85
|
+
!discriminatesKnownUnion(node) &&
|
|
57
86
|
(!allowInTypeGuards || !isInsideTypeGuard(node))) {
|
|
58
87
|
context.report({ node, messageId: "runtimeTypeof" });
|
|
59
88
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Adapted from dmmulroy/anti-slop
|
|
2
2
|
import { defineRule } from "@oxlint/plugins";
|
|
3
|
+
import { variableFor } from "../../../shared/index.js";
|
|
3
4
|
const FORBIDDEN_SYMBOL_NAME = "shape";
|
|
4
5
|
function containsForbiddenSymbolName(name) {
|
|
5
6
|
return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME);
|
|
@@ -11,6 +12,30 @@ function isBorrowedMemberName(node) {
|
|
|
11
12
|
}
|
|
12
13
|
return parent.property === node && !parent.computed;
|
|
13
14
|
}
|
|
15
|
+
function importedName(specifier) {
|
|
16
|
+
return specifier.imported.type === "Identifier"
|
|
17
|
+
? specifier.imported.name
|
|
18
|
+
: specifier.imported.value;
|
|
19
|
+
}
|
|
20
|
+
function isPackageImport(specifier) {
|
|
21
|
+
return (specifier.type === "ImportSpecifier" &&
|
|
22
|
+
specifier.parent.type === "ImportDeclaration" &&
|
|
23
|
+
!/^(?:\.{1,2}\/|@\/|~\/|#)/u.test(specifier.parent.source.value));
|
|
24
|
+
}
|
|
25
|
+
function isBorrowedImportName(sourceCode, node) {
|
|
26
|
+
const parent = node.parent;
|
|
27
|
+
if (parent?.type === "ImportSpecifier" && parent.imported === node) {
|
|
28
|
+
return isPackageImport(parent);
|
|
29
|
+
}
|
|
30
|
+
const variable = variableFor(sourceCode, node);
|
|
31
|
+
const definition = variable?.defs[0];
|
|
32
|
+
const specifier = definition?.type === "ImportBinding" ? definition.node : null;
|
|
33
|
+
return (specifier !== null &&
|
|
34
|
+
isPackageImport(specifier) &&
|
|
35
|
+
importedName(specifier) === node.name &&
|
|
36
|
+
(definition?.name === node ||
|
|
37
|
+
(variable?.references.some((reference) => reference.identifier === node) ?? false)));
|
|
38
|
+
}
|
|
14
39
|
export const noForbiddenTermInSymbolNames = defineRule({
|
|
15
40
|
meta: {
|
|
16
41
|
type: "problem",
|
|
@@ -23,7 +48,9 @@ export const noForbiddenTermInSymbolNames = defineRule({
|
|
|
23
48
|
},
|
|
24
49
|
createOnce(context) {
|
|
25
50
|
const reportForbiddenSymbolName = (node) => {
|
|
26
|
-
if (!containsForbiddenSymbolName(node.name) ||
|
|
51
|
+
if (!containsForbiddenSymbolName(node.name) ||
|
|
52
|
+
isBorrowedMemberName(node) ||
|
|
53
|
+
isBorrowedImportName(context.sourceCode, node)) {
|
|
27
54
|
return;
|
|
28
55
|
}
|
|
29
56
|
context.report({ node, messageId: "forbiddenSymbolName", data: { name: node.name } });
|
|
@@ -5,7 +5,7 @@ export const noUnknownReturns = defineRule({
|
|
|
5
5
|
meta: {
|
|
6
6
|
type: "problem",
|
|
7
7
|
docs: {
|
|
8
|
-
description: "Disallow functions whose
|
|
8
|
+
description: "Disallow functions whose declared return type is unknown or Promise<unknown>.",
|
|
9
9
|
},
|
|
10
10
|
messages: {
|
|
11
11
|
unknownReturn: "This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type.",
|
|
@@ -101,10 +101,10 @@ export const noUnsafeDictionaryType = defineRule({
|
|
|
101
101
|
meta: {
|
|
102
102
|
type: "problem",
|
|
103
103
|
docs: {
|
|
104
|
-
description: "Disallow
|
|
104
|
+
description: "Disallow dictionary types whose value type is unknown, any, object, {}, or a union or alias containing one of them.",
|
|
105
105
|
},
|
|
106
106
|
messages: {
|
|
107
|
-
unsafeDictionary: "This dictionary's {{value}} value type
|
|
107
|
+
unsafeDictionary: "This dictionary's {{value}} value type tells callers nothing about its values. Use the owner's or schema's value type, and parse external payloads before inserting them.",
|
|
108
108
|
},
|
|
109
109
|
},
|
|
110
110
|
createOnce(context) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTypeAliasEnvironment, hasVisibleTypeBinding, visibleTypeAlias, } from "./type-alias-resolution.js";
|
|
1
|
+
import { aliasSubstitutions, createTypeAliasEnvironment, hasVisibleTypeBinding, visibleTypeAlias, } from "./type-alias-resolution.js";
|
|
2
2
|
const BUILT_INS = new Set([
|
|
3
3
|
"Record",
|
|
4
4
|
"Readonly",
|
|
@@ -35,6 +35,9 @@ function typeReferenceName(type) {
|
|
|
35
35
|
function isBuiltIn(name, use, environment) {
|
|
36
36
|
return BUILT_INS.has(name) && !hasVisibleTypeBinding(name, use, environment.typeAliases);
|
|
37
37
|
}
|
|
38
|
+
function mappedKey(type) {
|
|
39
|
+
return type.nameType ?? type.constraint;
|
|
40
|
+
}
|
|
38
41
|
function unwrapTransparentType(type) {
|
|
39
42
|
let current = type;
|
|
40
43
|
while (current.type === "TSParenthesizedType" ||
|
|
@@ -43,12 +46,6 @@ function unwrapTransparentType(type) {
|
|
|
43
46
|
}
|
|
44
47
|
return current;
|
|
45
48
|
}
|
|
46
|
-
function isUnappliedReferenceTo(type, name) {
|
|
47
|
-
const unwrapped = unwrapTransparentType(type);
|
|
48
|
-
return (unwrapped.type === "TSTypeReference" &&
|
|
49
|
-
typeReferenceName(unwrapped) === name &&
|
|
50
|
-
(unwrapped.typeArguments === null || unwrapped.typeArguments.params.length === 0));
|
|
51
|
-
}
|
|
52
49
|
function isNeverType(type) {
|
|
53
50
|
return unwrapTransparentType(type).type === "TSNeverKeyword";
|
|
54
51
|
}
|
|
@@ -70,36 +67,6 @@ function isEffectivelyEmptyInterface(declarations) {
|
|
|
70
67
|
type.extends.length === 0 &&
|
|
71
68
|
(type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember)));
|
|
72
69
|
}
|
|
73
|
-
function resolvedSubstitutionArgument(type, base, resolving = new Set()) {
|
|
74
|
-
const unwrapped = unwrapTransparentType(type);
|
|
75
|
-
if (unwrapped.type !== "TSTypeReference") {
|
|
76
|
-
return type;
|
|
77
|
-
}
|
|
78
|
-
const name = typeReferenceName(unwrapped);
|
|
79
|
-
if (name === null || resolving.has(name)) {
|
|
80
|
-
return type;
|
|
81
|
-
}
|
|
82
|
-
const substitution = base.get(name);
|
|
83
|
-
if (substitution === void 0) {
|
|
84
|
-
return type;
|
|
85
|
-
}
|
|
86
|
-
const nextResolving = new Set(resolving);
|
|
87
|
-
nextResolving.add(name);
|
|
88
|
-
return resolvedSubstitutionArgument(substitution, base, nextResolving);
|
|
89
|
-
}
|
|
90
|
-
function aliasSubstitution(alias, type, base) {
|
|
91
|
-
const parameters = alias.typeParameters?.params ?? [];
|
|
92
|
-
const arguments_ = type.typeArguments?.params ?? [];
|
|
93
|
-
const next = new Map(base);
|
|
94
|
-
for (const [index, parameter] of parameters.entries()) {
|
|
95
|
-
const argument = arguments_[index] ?? parameter.default;
|
|
96
|
-
if (argument === null) {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
next.set(parameter.name.name, resolvedSubstitutionArgument(argument, next));
|
|
100
|
-
}
|
|
101
|
-
return next;
|
|
102
|
-
}
|
|
103
70
|
function unsafeDirectValue(type, environment, substitutions, resolvingAliases) {
|
|
104
71
|
const unwrapped = unwrapTransparentType(type);
|
|
105
72
|
if (unwrapped.type === "TSUnknownKeyword") {
|
|
@@ -144,9 +111,7 @@ function unsafeDirectValue(type, environment, substitutions, resolvingAliases) {
|
|
|
144
111
|
}
|
|
145
112
|
const substitution = substitutions.get(name);
|
|
146
113
|
if (substitution !== void 0) {
|
|
147
|
-
return
|
|
148
|
-
? null
|
|
149
|
-
: unsafeDirectValue(substitution, environment, substitutions, resolvingAliases);
|
|
114
|
+
return unsafeDirectValue(substitution.type, environment, substitution.substitutions, resolvingAliases);
|
|
150
115
|
}
|
|
151
116
|
const interfaceDeclarations = environment.interfaces.get(name);
|
|
152
117
|
if (interfaceDeclarations !== void 0) {
|
|
@@ -156,7 +121,7 @@ function unsafeDirectValue(type, environment, substitutions, resolvingAliases) {
|
|
|
156
121
|
if (alias === null || resolvingAliases.has(name)) {
|
|
157
122
|
return null;
|
|
158
123
|
}
|
|
159
|
-
const nextSubstitutions =
|
|
124
|
+
const nextSubstitutions = aliasSubstitutions(alias, unwrapped, substitutions);
|
|
160
125
|
if (nextSubstitutions === null) {
|
|
161
126
|
return null;
|
|
162
127
|
}
|
|
@@ -164,6 +129,50 @@ function unsafeDirectValue(type, environment, substitutions, resolvingAliases) {
|
|
|
164
129
|
nextResolving.add(name);
|
|
165
130
|
return unsafeDirectValue(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
|
|
166
131
|
}
|
|
132
|
+
function isBroadMappedKey(type, environment, substitutions, visitedAliases = new Set()) {
|
|
133
|
+
const unwrapped = unwrapTransparentType(type);
|
|
134
|
+
if (unwrapped.type === "TSStringKeyword" ||
|
|
135
|
+
unwrapped.type === "TSNumberKeyword" ||
|
|
136
|
+
unwrapped.type === "TSSymbolKeyword") {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (unwrapped.type === "TSUnionType") {
|
|
140
|
+
return unwrapped.types.some((member) => isBroadMappedKey(member, environment, substitutions, visitedAliases));
|
|
141
|
+
}
|
|
142
|
+
if (unwrapped.type !== "TSTypeReference") {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const name = typeReferenceName(unwrapped);
|
|
146
|
+
if (name === null) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const substitution = substitutions.get(name);
|
|
150
|
+
if (substitution !== void 0) {
|
|
151
|
+
return isBroadMappedKey(substitution.type, environment, substitution.substitutions, visitedAliases);
|
|
152
|
+
}
|
|
153
|
+
if (name === "PropertyKey" && isBuiltIn(name, unwrapped, environment)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
157
|
+
if (alias === null ||
|
|
158
|
+
(alias.typeParameters?.params.length ?? 0) > 0 ||
|
|
159
|
+
visitedAliases.has(name)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const nextVisited = new Set(visitedAliases);
|
|
163
|
+
nextVisited.add(name);
|
|
164
|
+
return isBroadMappedKey(alias.typeAnnotation, environment, substitutions, nextVisited);
|
|
165
|
+
}
|
|
166
|
+
function keptDictionarySource(type, environment, substitutions) {
|
|
167
|
+
const [source, keys] = type.typeArguments?.params ?? [];
|
|
168
|
+
if (source === void 0) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return typeReferenceName(type) === "Omit" ||
|
|
172
|
+
(keys !== void 0 && isBroadMappedKey(keys, environment, substitutions))
|
|
173
|
+
? source
|
|
174
|
+
: null;
|
|
175
|
+
}
|
|
167
176
|
function dictionaryValueTypes(type, environment, substitutions, resolvingAliases) {
|
|
168
177
|
const unwrapped = unwrapTransparentType(type);
|
|
169
178
|
if (unwrapped.type === "TSTypeLiteral") {
|
|
@@ -185,9 +194,7 @@ function dictionaryValueTypes(type, environment, substitutions, resolvingAliases
|
|
|
185
194
|
}
|
|
186
195
|
const substitution = substitutions.get(name);
|
|
187
196
|
if (substitution !== void 0) {
|
|
188
|
-
return
|
|
189
|
-
? []
|
|
190
|
-
: dictionaryValueTypes(substitution, environment, substitutions, resolvingAliases);
|
|
197
|
+
return dictionaryValueTypes(substitution.type, environment, substitution.substitutions, resolvingAliases);
|
|
191
198
|
}
|
|
192
199
|
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
193
200
|
const wrapped = unwrapped.typeArguments?.params[0];
|
|
@@ -200,8 +207,8 @@ function dictionaryValueTypes(type, environment, substitutions, resolvingAliases
|
|
|
200
207
|
return value === null ? [] : [{ type: value, substitutions }];
|
|
201
208
|
}
|
|
202
209
|
if ((name === "Pick" || name === "Omit") && isBuiltIn(name, unwrapped, environment)) {
|
|
203
|
-
const source = unwrapped
|
|
204
|
-
return source ===
|
|
210
|
+
const source = keptDictionarySource(unwrapped, environment, substitutions);
|
|
211
|
+
return source === null
|
|
205
212
|
? []
|
|
206
213
|
: dictionaryValueTypes(source, environment, substitutions, resolvingAliases);
|
|
207
214
|
}
|
|
@@ -209,7 +216,7 @@ function dictionaryValueTypes(type, environment, substitutions, resolvingAliases
|
|
|
209
216
|
if (alias === null || resolvingAliases.has(name)) {
|
|
210
217
|
return [];
|
|
211
218
|
}
|
|
212
|
-
const nextSubstitutions =
|
|
219
|
+
const nextSubstitutions = aliasSubstitutions(alias, unwrapped, substitutions);
|
|
213
220
|
if (nextSubstitutions === null) {
|
|
214
221
|
return [];
|
|
215
222
|
}
|
|
@@ -230,40 +237,6 @@ function classifyUnsafeDictionary(type, environment) {
|
|
|
230
237
|
}
|
|
231
238
|
return null;
|
|
232
239
|
}
|
|
233
|
-
function isBroadMappedKey(type, environment, substitutions, visitedAliases = new Set()) {
|
|
234
|
-
const unwrapped = unwrapTransparentType(type);
|
|
235
|
-
if (unwrapped.type === "TSStringKeyword" ||
|
|
236
|
-
unwrapped.type === "TSNumberKeyword" ||
|
|
237
|
-
unwrapped.type === "TSSymbolKeyword") {
|
|
238
|
-
return true;
|
|
239
|
-
}
|
|
240
|
-
if (unwrapped.type === "TSUnionType") {
|
|
241
|
-
return unwrapped.types.some((member) => isBroadMappedKey(member, environment, substitutions, visitedAliases));
|
|
242
|
-
}
|
|
243
|
-
if (unwrapped.type !== "TSTypeReference") {
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
const name = typeReferenceName(unwrapped);
|
|
247
|
-
if (name === null) {
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
const substitution = substitutions.get(name);
|
|
251
|
-
if (substitution !== void 0 && !isUnappliedReferenceTo(substitution, name)) {
|
|
252
|
-
return isBroadMappedKey(substitution, environment, substitutions, visitedAliases);
|
|
253
|
-
}
|
|
254
|
-
if (name === "PropertyKey" && isBuiltIn(name, unwrapped, environment)) {
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
258
|
-
if (alias === null ||
|
|
259
|
-
(alias.typeParameters?.params.length ?? 0) > 0 ||
|
|
260
|
-
visitedAliases.has(name)) {
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
const nextVisited = new Set(visitedAliases);
|
|
264
|
-
nextVisited.add(name);
|
|
265
|
-
return isBroadMappedKey(alias.typeAnnotation, environment, substitutions, nextVisited);
|
|
266
|
-
}
|
|
267
240
|
function hasBroadRecordKey(type, environment, substitutions) {
|
|
268
241
|
const key = type.typeArguments?.params[0];
|
|
269
242
|
return key === void 0 || isBroadMappedKey(key, environment, substitutions);
|
|
@@ -282,7 +255,7 @@ function classifyAliasBroadTarget(type, environment, substitutions, resolvingAli
|
|
|
282
255
|
: null;
|
|
283
256
|
}
|
|
284
257
|
if (unwrapped.type === "TSMappedType") {
|
|
285
|
-
return isBroadMappedKey(unwrapped
|
|
258
|
+
return isBroadMappedKey(mappedKey(unwrapped), environment, substitutions)
|
|
286
259
|
? { kind: "open dictionary" }
|
|
287
260
|
: null;
|
|
288
261
|
}
|
|
@@ -295,9 +268,7 @@ function classifyAliasBroadTarget(type, environment, substitutions, resolvingAli
|
|
|
295
268
|
}
|
|
296
269
|
const substitution = substitutions.get(name);
|
|
297
270
|
if (substitution !== void 0) {
|
|
298
|
-
return
|
|
299
|
-
? null
|
|
300
|
-
: classifyAliasBroadTarget(substitution, environment, substitutions, resolvingAliases);
|
|
271
|
+
return classifyAliasBroadTarget(substitution.type, environment, substitution.substitutions, resolvingAliases);
|
|
301
272
|
}
|
|
302
273
|
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
303
274
|
const wrapped = unwrapped.typeArguments?.params[0];
|
|
@@ -310,11 +281,18 @@ function classifyAliasBroadTarget(type, environment, substitutions, resolvingAli
|
|
|
310
281
|
? { kind: "open dictionary" }
|
|
311
282
|
: null;
|
|
312
283
|
}
|
|
284
|
+
if ((name === "Pick" || name === "Omit") && isBuiltIn(name, unwrapped, environment)) {
|
|
285
|
+
const source = keptDictionarySource(unwrapped, environment, substitutions);
|
|
286
|
+
const target = source === null
|
|
287
|
+
? null
|
|
288
|
+
: classifyAliasBroadTarget(source, environment, substitutions, resolvingAliases);
|
|
289
|
+
return target?.kind === "open dictionary" ? target : null;
|
|
290
|
+
}
|
|
313
291
|
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
314
292
|
if (alias === null || resolvingAliases.has(name)) {
|
|
315
293
|
return null;
|
|
316
294
|
}
|
|
317
|
-
const nextSubstitutions =
|
|
295
|
+
const nextSubstitutions = aliasSubstitutions(alias, unwrapped, substitutions);
|
|
318
296
|
if (nextSubstitutions === null) {
|
|
319
297
|
return null;
|
|
320
298
|
}
|
|
@@ -338,7 +316,9 @@ function classifyWideningTarget(type, environment) {
|
|
|
338
316
|
: null;
|
|
339
317
|
}
|
|
340
318
|
if (unwrapped.type === "TSMappedType") {
|
|
341
|
-
return
|
|
319
|
+
return isBroadMappedKey(mappedKey(unwrapped), environment, new Map())
|
|
320
|
+
? { kind: "open dictionary" }
|
|
321
|
+
: null;
|
|
342
322
|
}
|
|
343
323
|
if (unwrapped.type !== "TSTypeReference") {
|
|
344
324
|
return null;
|
|
@@ -356,18 +336,21 @@ function classifyWideningTarget(type, environment) {
|
|
|
356
336
|
? { kind: "open dictionary" }
|
|
357
337
|
: null;
|
|
358
338
|
}
|
|
339
|
+
if ((name === "Pick" || name === "Omit") && isBuiltIn(name, unwrapped, environment)) {
|
|
340
|
+
return classifyAliasBroadTarget(unwrapped, environment, new Map(), new Set());
|
|
341
|
+
}
|
|
359
342
|
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
360
343
|
if (alias === null) {
|
|
361
344
|
return null;
|
|
362
345
|
}
|
|
363
346
|
if ((alias.typeParameters?.params.length ?? 0) > 0) {
|
|
364
|
-
const substitutions2 =
|
|
347
|
+
const substitutions2 = aliasSubstitutions(alias, unwrapped, new Map());
|
|
365
348
|
const resolved2 = substitutions2 === null
|
|
366
349
|
? null
|
|
367
350
|
: classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions2, new Set([name]));
|
|
368
351
|
return resolved2?.kind === "open dictionary" ? { kind: "generic container" } : null;
|
|
369
352
|
}
|
|
370
|
-
const substitutions =
|
|
353
|
+
const substitutions = aliasSubstitutions(alias, unwrapped, new Map());
|
|
371
354
|
if (substitutions === null) {
|
|
372
355
|
return null;
|
|
373
356
|
}
|
|
@@ -7,6 +7,11 @@ type DeclaredTypeBinding = {
|
|
|
7
7
|
type TypeBinding = DeclaredTypeBinding & {
|
|
8
8
|
readonly scope: ESTree.Node;
|
|
9
9
|
};
|
|
10
|
+
type Substitution = {
|
|
11
|
+
readonly type: ESTree.TSType;
|
|
12
|
+
readonly substitutions: Substitutions;
|
|
13
|
+
};
|
|
14
|
+
type Substitutions = ReadonlyMap<string, Substitution>;
|
|
10
15
|
type TypeAliasEnvironment = {
|
|
11
16
|
readonly aliases: readonly ESTree.TSTypeAliasDeclaration[];
|
|
12
17
|
readonly bindingsByName: ReadonlyMap<string, readonly TypeBinding[]>;
|
|
@@ -16,6 +21,7 @@ type ResolvedTypeMatcher = (type: ESTree.TSType, matches: (child: ESTree.TSType)
|
|
|
16
21
|
declare function createTypeAliasEnvironment(program: ESTree.Program, visitorKeys: VisitorKeys): TypeAliasEnvironment;
|
|
17
22
|
declare function visibleTypeAlias(name: string, use: ESTree.Node, environment: TypeAliasEnvironment): ESTree.TSTypeAliasDeclaration | null;
|
|
18
23
|
declare function hasVisibleTypeBinding(name: string, use: ESTree.Node, environment: TypeAliasEnvironment): boolean;
|
|
24
|
+
declare function aliasSubstitutions(alias: ESTree.TSTypeAliasDeclaration, reference: ESTree.TSTypeReference, base: Substitutions): Substitutions | null;
|
|
19
25
|
declare function resolvedTypeMatches(type: ESTree.TSType, environment: TypeAliasEnvironment, matcher: ResolvedTypeMatcher): boolean;
|
|
20
|
-
export type { ResolvedTypeMatcher, TypeAliasEnvironment };
|
|
21
|
-
export { createTypeAliasEnvironment, hasVisibleTypeBinding, resolvedTypeMatches, visibleTypeAlias };
|
|
26
|
+
export type { ResolvedTypeMatcher, Substitution, Substitutions, TypeAliasEnvironment };
|
|
27
|
+
export { aliasSubstitutions, createTypeAliasEnvironment, hasVisibleTypeBinding, resolvedTypeMatches, visibleTypeAlias, };
|
|
@@ -105,7 +105,7 @@ function typeReferenceName(type) {
|
|
|
105
105
|
function aliasSubstitutions(alias, reference, base) {
|
|
106
106
|
const parameters = alias.typeParameters?.params ?? [];
|
|
107
107
|
const arguments_ = reference.typeArguments?.params ?? [];
|
|
108
|
-
const next = new Map(
|
|
108
|
+
const next = new Map();
|
|
109
109
|
for (const [index, parameter] of parameters.entries()) {
|
|
110
110
|
const explicitArgument = arguments_[index];
|
|
111
111
|
const argument = explicitArgument ?? parameter.default;
|
|
@@ -144,4 +144,4 @@ function resolvedTypeMatches(type, environment, matcher) {
|
|
|
144
144
|
};
|
|
145
145
|
return evaluate(type, new Map(), new Set());
|
|
146
146
|
}
|
|
147
|
-
export { createTypeAliasEnvironment, hasVisibleTypeBinding, resolvedTypeMatches, visibleTypeAlias };
|
|
147
|
+
export { aliasSubstitutions, createTypeAliasEnvironment, hasVisibleTypeBinding, resolvedTypeMatches, visibleTypeAlias, };
|