@commonplaces/oxlint-plugin 0.0.0-stage → 0.1.1

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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +130 -2
  3. package/dist/alchemy.d.ts +2 -0
  4. package/dist/alchemy.js +196 -0
  5. package/dist/anti-slop/effect/index.d.ts +3 -0
  6. package/dist/anti-slop/effect/index.js +10 -0
  7. package/dist/anti-slop/effect/rules/no-service-constructor-imports.d.ts +2 -0
  8. package/dist/anti-slop/effect/rules/no-service-constructor-imports.js +45 -0
  9. package/dist/anti-slop/index.d.ts +3 -0
  10. package/dist/anti-slop/index.js +38 -0
  11. package/dist/anti-slop/rules/no-chained-type-assertions.d.ts +2 -0
  12. package/dist/anti-slop/rules/no-chained-type-assertions.js +57 -0
  13. package/dist/anti-slop/rules/no-conditional-empty-object-spread.d.ts +2 -0
  14. package/dist/anti-slop/rules/no-conditional-empty-object-spread.js +36 -0
  15. package/dist/anti-slop/rules/no-known-value-widening.d.ts +2 -0
  16. package/dist/anti-slop/rules/no-known-value-widening.js +171 -0
  17. package/dist/anti-slop/rules/no-module-mocking.d.ts +2 -0
  18. package/dist/anti-slop/rules/no-module-mocking.js +73 -0
  19. package/dist/anti-slop/rules/no-object-parameters.d.ts +2 -0
  20. package/dist/anti-slop/rules/no-object-parameters.js +86 -0
  21. package/dist/anti-slop/rules/no-reflect-apply.d.ts +2 -0
  22. package/dist/anti-slop/rules/no-reflect-apply.js +25 -0
  23. package/dist/anti-slop/rules/no-reflect-get.d.ts +2 -0
  24. package/dist/anti-slop/rules/no-reflect-get.js +25 -0
  25. package/dist/anti-slop/rules/no-runtime-typeof.d.ts +2 -0
  26. package/dist/anti-slop/rules/no-runtime-typeof.js +47 -0
  27. package/dist/anti-slop/rules/no-shape-in-symbol-names.d.ts +2 -0
  28. package/dist/anti-slop/rules/no-shape-in-symbol-names.js +33 -0
  29. package/dist/anti-slop/rules/no-unknown-parameters.d.ts +2 -0
  30. package/dist/anti-slop/rules/no-unknown-parameters.js +69 -0
  31. package/dist/anti-slop/rules/no-unknown-returns.d.ts +2 -0
  32. package/dist/anti-slop/rules/no-unknown-returns.js +77 -0
  33. package/dist/anti-slop/rules/no-unknown-type-aliases.d.ts +2 -0
  34. package/dist/anti-slop/rules/no-unknown-type-aliases.js +58 -0
  35. package/dist/anti-slop/rules/no-unsafe-dictionary-type.d.ts +2 -0
  36. package/dist/anti-slop/rules/no-unsafe-dictionary-type.js +111 -0
  37. package/dist/anti-slop/rules/no-widen-then-assert.d.ts +2 -0
  38. package/dist/anti-slop/rules/no-widen-then-assert.js +255 -0
  39. package/dist/anti-slop/rules/require-safety-comment-for-type-assertion.d.ts +2 -0
  40. package/dist/anti-slop/rules/require-safety-comment-for-type-assertion.js +51 -0
  41. package/dist/anti-slop/shared/dictionary-types.d.ts +21 -0
  42. package/dist/anti-slop/shared/dictionary-types.js +374 -0
  43. package/dist/anti-slop/shared/lexical-type-parameters.d.ts +3 -0
  44. package/dist/anti-slop/shared/lexical-type-parameters.js +25 -0
  45. package/dist/anti-slop/shared/reflect-method.d.ts +3 -0
  46. package/dist/anti-slop/shared/reflect-method.js +24 -0
  47. package/dist/bun.d.ts +9 -0
  48. package/dist/bun.js +30 -0
  49. package/dist/effect.d.ts +17 -0
  50. package/dist/effect.js +352 -0
  51. package/dist/index.d.ts +57 -0
  52. package/dist/index.js +62 -0
  53. package/dist/package.d.ts +9 -0
  54. package/dist/package.js +50 -0
  55. package/dist/strict-effect.d.ts +2 -0
  56. package/dist/strict-effect.js +2381 -0
  57. package/dist/syntax.d.ts +5 -0
  58. package/dist/syntax.js +26 -0
  59. package/package.json +77 -4
@@ -0,0 +1,58 @@
1
+ import { Array, HashMap, Option } from "effect";
2
+ import { createTypeEnvironment } from "../shared/dictionary-types.js";
3
+ import { defineRule } from "@oxlint/plugins";
4
+ function referencedAliasName(type) {
5
+ if (type.type === "TSParenthesizedType")
6
+ return referencedAliasName(type.typeAnnotation);
7
+ if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier")
8
+ return null;
9
+ return type.typeArguments === null ||
10
+ type.typeArguments === undefined ||
11
+ type.typeArguments.params.length === 0
12
+ ? type.typeName.name
13
+ : null;
14
+ }
15
+ /** Ban named aliases that merely conceal TypeScript's unknown top type. */
16
+ export const noUnknownTypeAliasesRule = defineRule({
17
+ meta: {
18
+ type: "problem",
19
+ docs: {
20
+ description: "Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary.",
21
+ },
22
+ messages: {
23
+ unknownAlias: "Type alias `{{alias}}` hides `unknown`. Keep `unknown` explicit at the parsing boundary or on an allowed `cause` field; otherwise use the parsed owner type.",
24
+ },
25
+ },
26
+ create(context) {
27
+ const aliases = createTypeEnvironment(context.sourceCode.ast).aliases;
28
+ const resolvesToUnknown = (type, visited = []) => {
29
+ if (type.type === "TSUnknownKeyword")
30
+ return true;
31
+ if (type.type === "TSParenthesizedType")
32
+ return resolvesToUnknown(type.typeAnnotation, visited);
33
+ const name = referencedAliasName(type);
34
+ if (name === null || Array.contains(visited, name))
35
+ return false;
36
+ const alias = Option.getOrUndefined(HashMap.get(aliases, name));
37
+ if (alias === undefined ||
38
+ (alias.typeParameters !== null && alias.typeParameters !== undefined)) {
39
+ return false;
40
+ }
41
+ const nextVisited = [...visited, name];
42
+ return resolvesToUnknown(alias.typeAnnotation, nextVisited);
43
+ };
44
+ return {
45
+ Program() {
46
+ Array.forEach(HashMap.values(aliases), (alias) => {
47
+ if (!resolvesToUnknown(alias.typeAnnotation, [alias.id.name]))
48
+ return;
49
+ context.report({
50
+ node: alias.id,
51
+ messageId: "unknownAlias",
52
+ data: { alias: alias.id.name },
53
+ });
54
+ });
55
+ },
56
+ };
57
+ },
58
+ });
@@ -0,0 +1,2 @@
1
+ /** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
2
+ export declare const noUnsafeDictionaryTypeRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,111 @@
1
+ import { Array, HashMap } from "effect";
2
+ import { defineRule } from "@oxlint/plugins";
3
+ import { classifyUnsafeDictionary, classifyUnsafeDictionaryValue, createTypeEnvironment, } from "../shared/dictionary-types.js";
4
+ const typeNodeKinds = [
5
+ "JSDocNonNullableType",
6
+ "JSDocNullableType",
7
+ "JSDocUnknownType",
8
+ "TSAnyKeyword",
9
+ "TSArrayType",
10
+ "TSBigIntKeyword",
11
+ "TSBooleanKeyword",
12
+ "TSConditionalType",
13
+ "TSConstructorType",
14
+ "TSFunctionType",
15
+ "TSImportType",
16
+ "TSIndexedAccessType",
17
+ "TSInferType",
18
+ "TSIntersectionType",
19
+ "TSIntrinsicKeyword",
20
+ "TSLiteralType",
21
+ "TSMappedType",
22
+ "TSNamedTupleMember",
23
+ "TSNeverKeyword",
24
+ "TSNullKeyword",
25
+ "TSNumberKeyword",
26
+ "TSObjectKeyword",
27
+ "TSParenthesizedType",
28
+ "TSStringKeyword",
29
+ "TSSymbolKeyword",
30
+ "TSTemplateLiteralType",
31
+ "TSThisType",
32
+ "TSTupleType",
33
+ "TSTypeLiteral",
34
+ "TSTypeOperator",
35
+ "TSTypePredicate",
36
+ "TSTypeQuery",
37
+ "TSTypeReference",
38
+ "TSUndefinedKeyword",
39
+ "TSUnionType",
40
+ "TSUnknownKeyword",
41
+ "TSVoidKeyword",
42
+ ];
43
+ function isTypeNode(node) {
44
+ return Array.contains(typeNodeKinds, node.type);
45
+ }
46
+ function typeReferenceName(type) {
47
+ return type.typeName.type === "Identifier" ? type.typeName.name : null;
48
+ }
49
+ function isInsideTypeAliasDeclaration(node) {
50
+ const current = node.parent;
51
+ return (current !== null &&
52
+ current.type !== "Program" &&
53
+ (current.type === "TSTypeAliasDeclaration" || isInsideTypeAliasDeclaration(current)));
54
+ }
55
+ function isPlainAliasConsumerUse(node, environment) {
56
+ if (node.type !== "TSTypeReference" || node.typeArguments?.params.length)
57
+ return false;
58
+ const name = typeReferenceName(node);
59
+ return (name !== null && HashMap.has(environment.aliases, name) && !isInsideTypeAliasDeclaration(node));
60
+ }
61
+ function hasUnsafeParent(node, environment) {
62
+ return (node !== null &&
63
+ node.type !== "Program" &&
64
+ ((isTypeNode(node) && classifyUnsafeDictionary(node, environment) !== null) ||
65
+ hasUnsafeParent(node.parent, environment)));
66
+ }
67
+ function shouldReportType(node, environment) {
68
+ if (isPlainAliasConsumerUse(node, environment))
69
+ return false;
70
+ if (classifyUnsafeDictionary(node, environment) === null)
71
+ return false;
72
+ return !hasUnsafeParent(node.parent, environment);
73
+ }
74
+ /** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
75
+ export const noUnsafeDictionaryTypeRule = defineRule({
76
+ meta: {
77
+ type: "problem",
78
+ docs: {
79
+ description: "Disallow object-dictionary contracts whose direct value type is unknown, any, object, {}, or a union/alias containing one of those escape hatches.",
80
+ },
81
+ messages: {
82
+ unsafeDictionary: "This dictionary's {{value}} value type gives callers no concrete value contract. Use an owner/schema-derived value type; parse external payloads before insertion.",
83
+ },
84
+ },
85
+ create(context) {
86
+ const environment = createTypeEnvironment(context.sourceCode.ast);
87
+ const report = (node, value) => {
88
+ context.report({ node, messageId: "unsafeDictionary", data: { value } });
89
+ };
90
+ const reportIfUnsafe = (node) => {
91
+ if (!shouldReportType(node, environment))
92
+ return;
93
+ const unsafe = classifyUnsafeDictionary(node, environment);
94
+ if (unsafe === null)
95
+ return;
96
+ report(node, unsafe.unsafeValue);
97
+ };
98
+ return {
99
+ TSTypeReference: reportIfUnsafe,
100
+ TSTypeLiteral: reportIfUnsafe,
101
+ TSMappedType: reportIfUnsafe,
102
+ TSIndexSignature(node) {
103
+ if (node.typeAnnotation === null || node.parent.type === "TSTypeLiteral")
104
+ return;
105
+ const unsafe = classifyUnsafeDictionaryValue(node.typeAnnotation.typeAnnotation, environment);
106
+ if (unsafe !== null)
107
+ report(node, unsafe.unsafeValue);
108
+ },
109
+ };
110
+ },
111
+ });
@@ -0,0 +1,2 @@
1
+ /** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */
2
+ export declare const noWidenThenAssertRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,255 @@
1
+ import { Array, Option } from "effect";
2
+ import { defineRule } from "@oxlint/plugins";
3
+ const functionBoundaryTypes = [
4
+ "ArrowFunctionExpression",
5
+ "FunctionDeclaration",
6
+ "FunctionExpression",
7
+ "TSDeclareFunction",
8
+ "TSEmptyBodyFunctionExpression",
9
+ ];
10
+ function unwrapExpressionParentheses(expression) {
11
+ return expression.type === "ParenthesizedExpression"
12
+ ? unwrapExpressionParentheses(expression.expression)
13
+ : expression;
14
+ }
15
+ function unwrapTypeParentheses(type) {
16
+ return type.type === "TSParenthesizedType" ? unwrapTypeParentheses(type.typeAnnotation) : type;
17
+ }
18
+ function typeReferenceName(type) {
19
+ return type.typeName.type === "Identifier" ? type.typeName.name : null;
20
+ }
21
+ function isUnknownOrAnyType(type) {
22
+ const unwrapped = unwrapTypeParentheses(type);
23
+ return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword";
24
+ }
25
+ function isBroadRecordKeyType(type) {
26
+ const unwrapped = unwrapTypeParentheses(type);
27
+ if (unwrapped.type === "TSStringKeyword" ||
28
+ unwrapped.type === "TSNumberKeyword" ||
29
+ unwrapped.type === "TSSymbolKeyword") {
30
+ return true;
31
+ }
32
+ if (unwrapped.type === "TSUnionType")
33
+ return Array.every(unwrapped.types, isBroadRecordKeyType);
34
+ return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
35
+ }
36
+ function isBroadRecordType(type) {
37
+ const unwrapped = unwrapTypeParentheses(type);
38
+ if (unwrapped.type === "TSTypeReference") {
39
+ if (typeReferenceName(unwrapped) === "Readonly") {
40
+ const [inner] = unwrapped.typeArguments?.params ?? [];
41
+ return inner !== undefined && isBroadRecordType(inner);
42
+ }
43
+ if (typeReferenceName(unwrapped) !== "Record")
44
+ return false;
45
+ const parameters = unwrapped.typeArguments?.params ?? [];
46
+ const [key, value] = parameters;
47
+ return (parameters.length === 2 &&
48
+ key !== undefined &&
49
+ value !== undefined &&
50
+ isBroadRecordKeyType(key) &&
51
+ isUnknownOrAnyType(value));
52
+ }
53
+ if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1)
54
+ return false;
55
+ const [member] = unwrapped.members;
56
+ const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
57
+ return (member?.type === "TSIndexSignature" &&
58
+ member.parameters.length === 1 &&
59
+ parameter !== undefined &&
60
+ isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) &&
61
+ isUnknownOrAnyType(member.typeAnnotation.typeAnnotation));
62
+ }
63
+ function broadTypeKind(type) {
64
+ const unwrapped = unwrapTypeParentheses(type);
65
+ if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword")
66
+ return "top";
67
+ if (unwrapped.type === "TSObjectKeyword")
68
+ return "object";
69
+ return isBroadRecordType(unwrapped) ? "record" : null;
70
+ }
71
+ function assertedExpression(node) {
72
+ return unwrapExpressionParentheses(node.expression);
73
+ }
74
+ function assertionFromExpression(expression) {
75
+ const unwrapped = unwrapExpressionParentheses(expression);
76
+ return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion"
77
+ ? unwrapped
78
+ : null;
79
+ }
80
+ function normalizedTypeText(sourceText, type) {
81
+ return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, "");
82
+ }
83
+ function typesHaveSameSyntax(sourceText, left, right) {
84
+ return (left !== null &&
85
+ normalizedTypeText(sourceText, unwrapTypeParentheses(left)) ===
86
+ normalizedTypeText(sourceText, unwrapTypeParentheses(right)));
87
+ }
88
+ function isDefinitelyObjectType(type) {
89
+ const unwrapped = unwrapTypeParentheses(type);
90
+ if (Array.contains([
91
+ "TSArrayType",
92
+ "TSConstructorType",
93
+ "TSFunctionType",
94
+ "TSMappedType",
95
+ "TSObjectKeyword",
96
+ "TSTupleType",
97
+ ], unwrapped.type))
98
+ return true;
99
+ if (unwrapped.type === "TSTypeLiteral")
100
+ return unwrapped.members.length > 0;
101
+ if (unwrapped.type === "TSIntersectionType")
102
+ return Array.every(unwrapped.types, isDefinitelyObjectType);
103
+ return (unwrapped.type === "TSTypeOperator" &&
104
+ unwrapped.operator === "readonly" &&
105
+ isDefinitelyObjectType(unwrapped.typeAnnotation));
106
+ }
107
+ function isDefinitelyNarrowerRecordType(type) {
108
+ const unwrapped = unwrapTypeParentheses(type);
109
+ if (unwrapped.type === "TSTypeLiteral") {
110
+ return Array.some(unwrapped.members, (member) => member.type !== "TSIndexSignature");
111
+ }
112
+ if (unwrapped.type !== "TSTypeReference")
113
+ return false;
114
+ if (typeReferenceName(unwrapped) === "Readonly") {
115
+ const [inner] = unwrapped.typeArguments?.params ?? [];
116
+ return inner !== undefined && isDefinitelyNarrowerRecordType(inner);
117
+ }
118
+ if (typeReferenceName(unwrapped) !== "Record")
119
+ return false;
120
+ const parameters = unwrapped.typeArguments?.params ?? [];
121
+ const [, value] = parameters;
122
+ return parameters.length === 2 && value !== undefined && !isUnknownOrAnyType(value);
123
+ }
124
+ function functionBoundary(node) {
125
+ const current = node.parent;
126
+ if (current === null || current.type === "Program")
127
+ return null;
128
+ return Array.contains(functionBoundaryTypes, current.type) ? current : functionBoundary(current);
129
+ }
130
+ function resolvedVariableForIdentifier(scopes, identifier) {
131
+ return Option.getOrNull(Array.findFirst(scopes, (scope) => Array.some(scope.references, (candidate) => candidate.identifier.start === identifier.start &&
132
+ candidate.identifier.end === identifier.end)).pipe(Option.flatMap((scope) => Array.findFirst(scope.references, (candidate) => candidate.identifier.start === identifier.start &&
133
+ candidate.identifier.end === identifier.end)), Option.flatMap((reference) => Option.fromNullishOr(reference.resolved))));
134
+ }
135
+ function variableDeclarator(variable) {
136
+ return Option.getOrNull(Array.findFirst(variable.defs, (definition) => definition.type === "Variable" && definition.node.type === "VariableDeclarator").pipe(Option.flatMap((definition) => definition.node.type === "VariableDeclarator"
137
+ ? Option.some(definition.node)
138
+ : Option.none())));
139
+ }
140
+ function knownValueEvidence(expression, scopes, boundary, visitedVariables) {
141
+ const unwrapped = unwrapExpressionParentheses(expression);
142
+ if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
143
+ if (broadTypeKind(unwrapped.typeAnnotation) !== null)
144
+ return null;
145
+ return { type: unwrapped.typeAnnotation };
146
+ }
147
+ if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") {
148
+ return { type: null };
149
+ }
150
+ if (unwrapped.type === "ArrayExpression" ||
151
+ unwrapped.type === "ArrowFunctionExpression" ||
152
+ unwrapped.type === "ClassExpression" ||
153
+ unwrapped.type === "FunctionExpression" ||
154
+ unwrapped.type === "NewExpression" ||
155
+ unwrapped.type === "ObjectExpression") {
156
+ return { type: null };
157
+ }
158
+ if (unwrapped.type !== "Identifier")
159
+ return null;
160
+ const variable = resolvedVariableForIdentifier(scopes, unwrapped);
161
+ if (variable === null || Array.some(visitedVariables, (previous) => previous === variable))
162
+ return null;
163
+ const annotatedIdentifier = Option.getOrUndefined(Array.findFirst(variable.identifiers, (identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== undefined));
164
+ const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
165
+ if (annotation !== undefined && annotatedIdentifier !== undefined) {
166
+ if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) {
167
+ return null;
168
+ }
169
+ return { type: annotation };
170
+ }
171
+ const declarator = variableDeclarator(variable);
172
+ if (declarator === null ||
173
+ declarator.parent.type !== "VariableDeclaration" ||
174
+ declarator.parent.kind !== "const" ||
175
+ declarator.init === null ||
176
+ Array.some(variable.references, (reference) => reference.isWrite() && !reference.init) ||
177
+ functionBoundary(declarator) !== boundary) {
178
+ return null;
179
+ }
180
+ return knownValueEvidence(declarator.init, scopes, boundary, [...visitedVariables, variable]);
181
+ }
182
+ function widenedBinding(variable, scopes) {
183
+ const declarator = variableDeclarator(variable);
184
+ if (declarator === null ||
185
+ declarator.parent.type !== "VariableDeclaration" ||
186
+ declarator.parent.kind !== "const" ||
187
+ declarator.id.type !== "Identifier" ||
188
+ declarator.init === null ||
189
+ Array.some(variable.references, (reference) => reference.isWrite() && !reference.init)) {
190
+ return null;
191
+ }
192
+ const boundary = functionBoundary(declarator);
193
+ const declaredType = declarator.id.typeAnnotation?.typeAnnotation;
194
+ const initializerAssertion = assertionFromExpression(declarator.init);
195
+ const initializerBroadKind = initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
196
+ const declaredBroadKind = declaredType === undefined ? null : broadTypeKind(declaredType);
197
+ const broadKind = declaredBroadKind ?? initializerBroadKind;
198
+ if (broadKind === null)
199
+ return null;
200
+ const originalExpression = initializerAssertion !== null && initializerBroadKind !== null
201
+ ? assertedExpression(initializerAssertion)
202
+ : declarator.init;
203
+ const evidence = knownValueEvidence(originalExpression, scopes, boundary, [variable]);
204
+ return evidence === null ? null : { broadKind, evidence, declaredAt: declarator.end, boundary };
205
+ }
206
+ function assertionIsNarrower(sourceText, broadKind, evidence, assertedType) {
207
+ if (broadTypeKind(assertedType) !== null)
208
+ return false;
209
+ if (broadKind === "top")
210
+ return true;
211
+ if (typesHaveSameSyntax(sourceText, evidence.type, assertedType))
212
+ return true;
213
+ if (broadKind === "object")
214
+ return isDefinitelyObjectType(assertedType);
215
+ return isDefinitelyNarrowerRecordType(assertedType);
216
+ }
217
+ /** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */
218
+ export const noWidenThenAssertRule = defineRule({
219
+ meta: {
220
+ type: "problem",
221
+ docs: {
222
+ description: "Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type.",
223
+ },
224
+ messages: {
225
+ widenThenAssert: 'Binding "{{name}}" discards type evidence and later recreates it with an assertion. Keep the precise type from initialization through use; parse boundary input once.',
226
+ },
227
+ },
228
+ create(context) {
229
+ const scopes = context.sourceCode.scopeManager.scopes;
230
+ const checkAssertion = (node) => {
231
+ const expression = assertedExpression(node);
232
+ if (expression.type !== "Identifier")
233
+ return;
234
+ const variable = resolvedVariableForIdentifier(scopes, expression);
235
+ if (variable === null)
236
+ return;
237
+ const widened = widenedBinding(variable, scopes);
238
+ if (widened === null ||
239
+ node.start <= widened.declaredAt ||
240
+ functionBoundary(node) !== widened.boundary ||
241
+ !assertionIsNarrower(context.sourceCode.text, widened.broadKind, widened.evidence, node.typeAnnotation)) {
242
+ return;
243
+ }
244
+ context.report({
245
+ node,
246
+ messageId: "widenThenAssert",
247
+ data: { name: expression.name },
248
+ });
249
+ };
250
+ return {
251
+ TSAsExpression: checkAssertion,
252
+ TSTypeAssertion: checkAssertion,
253
+ };
254
+ },
255
+ });
@@ -0,0 +1,2 @@
1
+ /** Require every non-const type assertion to state the invariant TypeScript cannot express. */
2
+ export declare const requireSafetyCommentForTypeAssertionRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,51 @@
1
+ import { Array } from "effect";
2
+ import { defineRule } from "@oxlint/plugins";
3
+ const commentOwnerKinds = [
4
+ "ExportNamedDeclaration",
5
+ "ExportDefaultDeclaration",
6
+ "ExpressionStatement",
7
+ "PropertyDefinition",
8
+ "ReturnStatement",
9
+ "ThrowStatement",
10
+ "VariableDeclaration",
11
+ ];
12
+ function isConstAssertion(node) {
13
+ return (node.typeAnnotation.type === "TSTypeReference" &&
14
+ node.typeAnnotation.typeName.type === "Identifier" &&
15
+ node.typeAnnotation.typeName.name === "const");
16
+ }
17
+ function hasSafetyComment(sourceCode, node, current = node) {
18
+ if (Array.some(sourceCode.getCommentsBefore(current), (comment) => comment.end <= node.start && /\bSAFETY\s*:/u.test(comment.value)))
19
+ return true;
20
+ const parent = current.parent;
21
+ if (parent === null || parent.type === "Program")
22
+ return false;
23
+ if (Array.contains(commentOwnerKinds, current.type) &&
24
+ parent.type !== "ExportNamedDeclaration" &&
25
+ parent.type !== "ExportDefaultDeclaration")
26
+ return false;
27
+ return hasSafetyComment(sourceCode, node, current.parent);
28
+ }
29
+ /** Require every non-const type assertion to state the invariant TypeScript cannot express. */
30
+ export const requireSafetyCommentForTypeAssertionRule = defineRule({
31
+ meta: {
32
+ type: "problem",
33
+ docs: {
34
+ description: "Require a nearby SAFETY comment for every TypeScript type assertion except const assertions.",
35
+ },
36
+ messages: {
37
+ missingSafetyComment: "This type assertion has no `SAFETY:` justification. State the checked invariant immediately before the assertion or its containing statement.",
38
+ },
39
+ },
40
+ createOnce(context) {
41
+ const checkAssertion = (node) => {
42
+ if (isConstAssertion(node) || hasSafetyComment(context.sourceCode, node))
43
+ return;
44
+ context.report({ node, messageId: "missingSafetyComment" });
45
+ };
46
+ return {
47
+ TSAsExpression: checkAssertion,
48
+ TSTypeAssertion: checkAssertion,
49
+ };
50
+ },
51
+ });
@@ -0,0 +1,21 @@
1
+ import { HashMap, HashSet } from "effect";
2
+ import type { ESTree } from "@oxlint/plugins";
3
+ export type UnsafeDictionary = {
4
+ readonly kind: "unsafe-dictionary";
5
+ readonly unsafeValue: "any" | "empty-object" | "object" | "union" | "unknown";
6
+ };
7
+ type WideningTargetKind = "anonymous object" | "generic container" | "object" | "open dictionary" | "unknown";
8
+ export type WideningTarget = {
9
+ readonly kind: WideningTargetKind;
10
+ };
11
+ export type TypeEnvironment = {
12
+ readonly aliases: HashMap.HashMap<string, ESTree.TSTypeAliasDeclaration>;
13
+ readonly interfaces: HashMap.HashMap<string, readonly ESTree.TSInterfaceDeclaration[]>;
14
+ readonly shadowedBuiltIns: HashSet.HashSet<string>;
15
+ };
16
+ export declare function createTypeEnvironment(program: ESTree.Program): TypeEnvironment;
17
+ export declare function classifyUnsafeDictionaryValue(valueType: ESTree.TSType, environment: TypeEnvironment): UnsafeDictionary | null;
18
+ export declare function classifyUnsafeDictionary(type: ESTree.TSType, environment: TypeEnvironment): UnsafeDictionary | null;
19
+ export declare function classifyWideningTarget(type: ESTree.TSType, environment: TypeEnvironment): WideningTarget | null;
20
+ export declare function isKnownEvidenceExpression(expression: ESTree.Expression): boolean;
21
+ export {};