@jameswyse/oxc-config 0.1.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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +213 -0
  3. package/REFERENCE.md +134 -0
  4. package/dist/format.d.ts +5 -0
  5. package/dist/format.js +47 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +97 -0
  8. package/dist/lint.d.ts +33 -0
  9. package/dist/lint.js +493 -0
  10. package/dist/rules/companion-has-owner.d.ts +1 -0
  11. package/dist/rules/companion-has-owner.js +74 -0
  12. package/dist/rules/electron-no-raw-ipc-renderer-exposure.d.ts +1 -0
  13. package/dist/rules/electron-no-raw-ipc-renderer-exposure.js +127 -0
  14. package/dist/rules/electron-no-remote-module.d.ts +1 -0
  15. package/dist/rules/electron-no-remote-module.js +112 -0
  16. package/dist/rules/electron-secure-web-preferences.d.ts +1 -0
  17. package/dist/rules/electron-secure-web-preferences.js +108 -0
  18. package/dist/rules/explicit-api-states.d.ts +1 -0
  19. package/dist/rules/explicit-api-states.js +143 -0
  20. package/dist/rules/import-boundaries.d.ts +1 -0
  21. package/dist/rules/import-boundaries.js +99 -0
  22. package/dist/rules/no-ambient-clock.d.ts +1 -0
  23. package/dist/rules/no-ambient-clock.js +35 -0
  24. package/dist/rules/no-ambient-random.d.ts +1 -0
  25. package/dist/rules/no-ambient-random.js +71 -0
  26. package/dist/rules/no-comments.d.ts +1 -0
  27. package/dist/rules/no-comments.js +64 -0
  28. package/dist/rules/no-deep-ternary.d.ts +1 -0
  29. package/dist/rules/no-deep-ternary.js +32 -0
  30. package/dist/rules/no-edit-proof-assertions.d.ts +1 -0
  31. package/dist/rules/no-edit-proof-assertions.js +120 -0
  32. package/dist/rules/no-env-access.d.ts +1 -0
  33. package/dist/rules/no-env-access.js +35 -0
  34. package/dist/rules/no-fixed-test-delay.d.ts +1 -0
  35. package/dist/rules/no-fixed-test-delay.js +130 -0
  36. package/dist/rules/no-forwarding-module.d.ts +1 -0
  37. package/dist/rules/no-forwarding-module.js +158 -0
  38. package/dist/rules/no-generic-file-suffix.d.ts +1 -0
  39. package/dist/rules/no-generic-file-suffix.js +74 -0
  40. package/dist/rules/no-pass-through-function.d.ts +1 -0
  41. package/dist/rules/no-pass-through-function.js +106 -0
  42. package/dist/rules/no-swallowed-errors.d.ts +1 -0
  43. package/dist/rules/no-swallowed-errors.js +168 -0
  44. package/dist/rules/no-test-timeout-overrides.d.ts +1 -0
  45. package/dist/rules/no-test-timeout-overrides.js +105 -0
  46. package/dist/rules/no-vacuous-test-assertions.d.ts +1 -0
  47. package/dist/rules/no-vacuous-test-assertions.js +71 -0
  48. package/dist/rules/server-action-guard.d.ts +1 -0
  49. package/dist/rules/server-action-guard.js +109 -0
  50. package/dist/rules/statement-spacing.d.ts +1 -0
  51. package/dist/rules/statement-spacing.js +29 -0
  52. package/dist/rules/type-guard-name.d.ts +1 -0
  53. package/dist/rules/type-guard-name.js +59 -0
  54. package/dist/shared/index.d.ts +26 -0
  55. package/dist/shared/index.js +260 -0
  56. package/dist/shared/rule-options.d.ts +2 -0
  57. package/dist/shared/rule-options.js +8 -0
  58. package/dist/vendor/anti-slop/effect/rules/no-manual-effect-error-tag.d.ts +1 -0
  59. package/dist/vendor/anti-slop/effect/rules/no-manual-effect-error-tag.js +35 -0
  60. package/dist/vendor/anti-slop/effect/rules/no-service-constructor-imports.d.ts +1 -0
  61. package/dist/vendor/anti-slop/effect/rules/no-service-constructor-imports.js +48 -0
  62. package/dist/vendor/anti-slop/effect/shared/tagged-values.d.ts +9 -0
  63. package/dist/vendor/anti-slop/effect/shared/tagged-values.js +65 -0
  64. package/dist/vendor/anti-slop/rules/no-array-filter-map.d.ts +1 -0
  65. package/dist/vendor/anti-slop/rules/no-array-filter-map.js +40 -0
  66. package/dist/vendor/anti-slop/rules/no-chained-type-assertions.d.ts +1 -0
  67. package/dist/vendor/anti-slop/rules/no-chained-type-assertions.js +58 -0
  68. package/dist/vendor/anti-slop/rules/no-conditional-empty-object-spread.d.ts +1 -0
  69. package/dist/vendor/anti-slop/rules/no-conditional-empty-object-spread.js +41 -0
  70. package/dist/vendor/anti-slop/rules/no-known-value-widening.d.ts +1 -0
  71. package/dist/vendor/anti-slop/rules/no-known-value-widening.js +333 -0
  72. package/dist/vendor/anti-slop/rules/no-module-mocking.d.ts +1 -0
  73. package/dist/vendor/anti-slop/rules/no-module-mocking.js +74 -0
  74. package/dist/vendor/anti-slop/rules/no-object-parameters.d.ts +1 -0
  75. package/dist/vendor/anti-slop/rules/no-object-parameters.js +59 -0
  76. package/dist/vendor/anti-slop/rules/no-reduce-accumulator-copy.d.ts +1 -0
  77. package/dist/vendor/anti-slop/rules/no-reduce-accumulator-copy.js +124 -0
  78. package/dist/vendor/anti-slop/rules/no-reflect-apply.d.ts +1 -0
  79. package/dist/vendor/anti-slop/rules/no-reflect-apply.js +26 -0
  80. package/dist/vendor/anti-slop/rules/no-reflect-get.d.ts +1 -0
  81. package/dist/vendor/anti-slop/rules/no-reflect-get.js +26 -0
  82. package/dist/vendor/anti-slop/rules/no-runtime-typeof.d.ts +1 -0
  83. package/dist/vendor/anti-slop/rules/no-runtime-typeof.js +63 -0
  84. package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.d.ts +1 -0
  85. package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.js +37 -0
  86. package/dist/vendor/anti-slop/rules/no-unknown-returns.d.ts +1 -0
  87. package/dist/vendor/anti-slop/rules/no-unknown-returns.js +61 -0
  88. package/dist/vendor/anti-slop/rules/no-unknown-type-aliases.d.ts +1 -0
  89. package/dist/vendor/anti-slop/rules/no-unknown-type-aliases.js +38 -0
  90. package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.d.ts +1 -0
  91. package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.js +143 -0
  92. package/dist/vendor/anti-slop/rules/no-widen-then-assert.d.ts +1 -0
  93. package/dist/vendor/anti-slop/rules/no-widen-then-assert.js +288 -0
  94. package/dist/vendor/anti-slop/shared/array-method.d.ts +10 -0
  95. package/dist/vendor/anti-slop/shared/array-method.js +95 -0
  96. package/dist/vendor/anti-slop/shared/dictionary-types.d.ts +23 -0
  97. package/dist/vendor/anti-slop/shared/dictionary-types.js +408 -0
  98. package/dist/vendor/anti-slop/shared/function-parameters.d.ts +6 -0
  99. package/dist/vendor/anti-slop/shared/function-parameters.js +41 -0
  100. package/dist/vendor/anti-slop/shared/lexical-type-parameters.d.ts +4 -0
  101. package/dist/vendor/anti-slop/shared/lexical-type-parameters.js +32 -0
  102. package/dist/vendor/anti-slop/shared/reflect-method.d.ts +3 -0
  103. package/dist/vendor/anti-slop/shared/reflect-method.js +24 -0
  104. package/dist/vendor/anti-slop/shared/scope.d.ts +3 -0
  105. package/dist/vendor/anti-slop/shared/scope.js +12 -0
  106. package/dist/vendor/anti-slop/shared/type-alias-resolution.d.ts +21 -0
  107. package/dist/vendor/anti-slop/shared/type-alias-resolution.js +147 -0
  108. package/dist/vendor/eslint-stylistic/padding-line-ast.d.ts +14 -0
  109. package/dist/vendor/eslint-stylistic/padding-line-ast.js +19 -0
  110. package/dist/vendor/eslint-stylistic/padding-line-between-statements.d.ts +91 -0
  111. package/dist/vendor/eslint-stylistic/padding-line-between-statements.js +493 -0
  112. package/package.json +89 -0
  113. package/src/vendor/anti-slop/LICENSE +21 -0
  114. package/src/vendor/eslint-stylistic/LICENSE +22 -0
  115. package/tsconfig.base.json +10 -0
@@ -0,0 +1,26 @@
1
+ // Adapted from dmmulroy/anti-slop
2
+ import { defineRule } from "@oxlint/plugins";
3
+ import { isGlobalReflectMethodCall } from "../shared/reflect-method.js";
4
+ export const noReflectGet = defineRule({
5
+ meta: {
6
+ type: "problem",
7
+ docs: {
8
+ description: "Disallow Reflect.get; use typed property access or parse dynamic input into a domain type.",
9
+ },
10
+ messages: {
11
+ reflectGet: "Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type before reading it.",
12
+ },
13
+ },
14
+ createOnce(context) {
15
+ return {
16
+ CallExpression(node) {
17
+ if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") {
18
+ return;
19
+ }
20
+ if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "get")) {
21
+ context.report({ node, messageId: "reflectGet" });
22
+ }
23
+ },
24
+ };
25
+ },
26
+ });
@@ -0,0 +1 @@
1
+ export declare const noRuntimeTypeof: import("@oxlint/plugins").Rule;
@@ -0,0 +1,63 @@
1
+ // Adapted from dmmulroy/anti-slop
2
+ import { defineRule } from "@oxlint/plugins";
3
+ function isRuntimeFunction(node) {
4
+ return (node.type === "ArrowFunctionExpression" ||
5
+ node.type === "FunctionDeclaration" ||
6
+ node.type === "FunctionExpression");
7
+ }
8
+ function isInsideTypeGuard(node) {
9
+ let current = node.parent;
10
+ while (current !== null && current.type !== "Program") {
11
+ if (isRuntimeFunction(current)) {
12
+ return current.returnType?.typeAnnotation.type === "TSTypePredicate";
13
+ }
14
+ current = current.parent;
15
+ }
16
+ return false;
17
+ }
18
+ function isExistenceProbe(node) {
19
+ const parent = node.parent;
20
+ if (parent.type !== "BinaryExpression") {
21
+ return false;
22
+ }
23
+ if (!["===", "!==", "==", "!="].includes(parent.operator)) {
24
+ return false;
25
+ }
26
+ const other = parent.left === node ? parent.right : parent.left;
27
+ return other.type === "Literal" && other.value === "undefined";
28
+ }
29
+ export const noRuntimeTypeof = defineRule({
30
+ meta: {
31
+ type: "problem",
32
+ docs: {
33
+ description: "Disallow runtime typeof checks; external values must be decoded into meaningful types at their I/O boundary.",
34
+ },
35
+ messages: {
36
+ runtimeTypeof: "A `typeof` check narrows a representation without establishing its contract. Parse input at its I/O boundary, then branch on the domain value.",
37
+ },
38
+ schema: [
39
+ {
40
+ type: "object",
41
+ properties: { allowInTypeGuards: { type: "boolean" } },
42
+ additionalProperties: false,
43
+ },
44
+ ],
45
+ defaultOptions: [{ allowInTypeGuards: false }],
46
+ },
47
+ createOnce(context) {
48
+ return {
49
+ UnaryExpression(node) {
50
+ const option = context.options[0];
51
+ const allowInTypeGuards = typeof option === "object" &&
52
+ option !== null &&
53
+ !Array.isArray(option) &&
54
+ option.allowInTypeGuards === true;
55
+ if (node.operator === "typeof" &&
56
+ !isExistenceProbe(node) &&
57
+ (!allowInTypeGuards || !isInsideTypeGuard(node))) {
58
+ context.report({ node, messageId: "runtimeTypeof" });
59
+ }
60
+ },
61
+ };
62
+ },
63
+ });
@@ -0,0 +1 @@
1
+ export declare const noForbiddenTermInSymbolNames: import("@oxlint/plugins").Rule;
@@ -0,0 +1,37 @@
1
+ // Adapted from dmmulroy/anti-slop
2
+ import { defineRule } from "@oxlint/plugins";
3
+ const FORBIDDEN_SYMBOL_NAME = "shape";
4
+ function containsForbiddenSymbolName(name) {
5
+ return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME);
6
+ }
7
+ function isBorrowedMemberName(node) {
8
+ const parent = node.parent;
9
+ if (parent === null || parent.type !== "MemberExpression") {
10
+ return false;
11
+ }
12
+ return parent.property === node && !parent.computed;
13
+ }
14
+ export const noForbiddenTermInSymbolNames = defineRule({
15
+ meta: {
16
+ type: "problem",
17
+ docs: {
18
+ description: 'Disallow the case-insensitive substring "shape" in JavaScript, TypeScript, private, and JSX symbol names.',
19
+ },
20
+ messages: {
21
+ forbiddenSymbolName: 'Rename symbol "{{name}}" for its domain role; "shape" describes structure rather than ownership.',
22
+ },
23
+ },
24
+ createOnce(context) {
25
+ const reportForbiddenSymbolName = (node) => {
26
+ if (!containsForbiddenSymbolName(node.name) || isBorrowedMemberName(node)) {
27
+ return;
28
+ }
29
+ context.report({ node, messageId: "forbiddenSymbolName", data: { name: node.name } });
30
+ };
31
+ return {
32
+ Identifier: reportForbiddenSymbolName,
33
+ PrivateIdentifier: reportForbiddenSymbolName,
34
+ JSXIdentifier: reportForbiddenSymbolName,
35
+ };
36
+ },
37
+ });
@@ -0,0 +1 @@
1
+ export declare const noUnknownReturns: import("@oxlint/plugins").Rule;
@@ -0,0 +1,61 @@
1
+ // Adapted from dmmulroy/anti-slop
2
+ import { defineRule } from "@oxlint/plugins";
3
+ import { createTypeAliasEnvironment, resolvedTypeMatches, } from "../shared/type-alias-resolution.js";
4
+ export const noUnknownReturns = defineRule({
5
+ meta: {
6
+ type: "problem",
7
+ docs: {
8
+ description: "Disallow functions whose explicit return contract is unknown or Promise<unknown>.",
9
+ },
10
+ messages: {
11
+ unknownReturn: "This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type.",
12
+ },
13
+ },
14
+ createOnce(context) {
15
+ let environment = null;
16
+ const resolvesToUnknown = (type) => environment !== null &&
17
+ resolvedTypeMatches(type, environment, (resolved, matches) => {
18
+ if (resolved.type === "TSUnknownKeyword") {
19
+ return true;
20
+ }
21
+ if (resolved.type === "TSParenthesizedType") {
22
+ return matches(resolved.typeAnnotation);
23
+ }
24
+ if (resolved.type === "TSUnionType") {
25
+ return resolved.types.some(matches);
26
+ }
27
+ if (resolved.type !== "TSTypeReference" ||
28
+ resolved.typeName.type !== "Identifier" ||
29
+ (resolved.typeName.name !== "Promise" && resolved.typeName.name !== "PromiseLike")) {
30
+ return false;
31
+ }
32
+ const value = resolved.typeArguments?.params[0];
33
+ return value !== void 0 && matches(value);
34
+ });
35
+ const checkReturnType = (node) => {
36
+ const annotation = node.returnType;
37
+ if (annotation === null || annotation === void 0) {
38
+ return;
39
+ }
40
+ if (!resolvesToUnknown(annotation.typeAnnotation)) {
41
+ return;
42
+ }
43
+ context.report({ node: annotation.typeAnnotation, messageId: "unknownReturn" });
44
+ };
45
+ return {
46
+ Program(node) {
47
+ environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
48
+ },
49
+ ArrowFunctionExpression: checkReturnType,
50
+ FunctionDeclaration: checkReturnType,
51
+ FunctionExpression: checkReturnType,
52
+ TSCallSignatureDeclaration: checkReturnType,
53
+ TSConstructSignatureDeclaration: checkReturnType,
54
+ TSConstructorType: checkReturnType,
55
+ TSDeclareFunction: checkReturnType,
56
+ TSEmptyBodyFunctionExpression: checkReturnType,
57
+ TSFunctionType: checkReturnType,
58
+ TSMethodSignature: checkReturnType,
59
+ };
60
+ },
61
+ });
@@ -0,0 +1 @@
1
+ export declare const noUnknownTypeAliases: import("@oxlint/plugins").Rule;
@@ -0,0 +1,38 @@
1
+ // Adapted from dmmulroy/anti-slop
2
+ import { defineRule } from "@oxlint/plugins";
3
+ import { createTypeAliasEnvironment, resolvedTypeMatches, } from "../shared/type-alias-resolution.js";
4
+ export const noUnknownTypeAliases = defineRule({
5
+ meta: {
6
+ type: "problem",
7
+ docs: {
8
+ description: "Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary.",
9
+ },
10
+ messages: {
11
+ 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.",
12
+ },
13
+ },
14
+ createOnce(context) {
15
+ let environment = null;
16
+ const resolvesToUnknown = (type) => environment !== null &&
17
+ resolvedTypeMatches(type, environment, (resolved, matches) => {
18
+ if (resolved.type === "TSUnknownKeyword") {
19
+ return true;
20
+ }
21
+ if (resolved.type === "TSParenthesizedType") {
22
+ return matches(resolved.typeAnnotation);
23
+ }
24
+ return resolved.type === "TSUnionType" && resolved.types.some(matches);
25
+ });
26
+ return {
27
+ Program(node) {
28
+ environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
29
+ },
30
+ TSTypeAliasDeclaration(node) {
31
+ if (!resolvesToUnknown(node.typeAnnotation)) {
32
+ return;
33
+ }
34
+ context.report({ node: node.id, messageId: "unknownAlias", data: { alias: node.id.name } });
35
+ },
36
+ };
37
+ },
38
+ });
@@ -0,0 +1 @@
1
+ export declare const noUnsafeDictionaryType: import("@oxlint/plugins").Rule;
@@ -0,0 +1,143 @@
1
+ // Adapted from dmmulroy/anti-slop
2
+ import { defineRule } from "@oxlint/plugins";
3
+ import { classifyUnsafeDictionary, classifyUnsafeDictionaryValue, createTypeEnvironment, } from "../shared/dictionary-types.js";
4
+ import { visibleTypeAlias } from "../shared/type-alias-resolution.js";
5
+ const typeNodeKinds = new Set([
6
+ "TSJSDocNonNullableType",
7
+ "TSJSDocNullableType",
8
+ "TSJSDocUnknownType",
9
+ "TSAnyKeyword",
10
+ "TSArrayType",
11
+ "TSBigIntKeyword",
12
+ "TSBooleanKeyword",
13
+ "TSConditionalType",
14
+ "TSConstructorType",
15
+ "TSFunctionType",
16
+ "TSImportType",
17
+ "TSIndexedAccessType",
18
+ "TSInferType",
19
+ "TSIntersectionType",
20
+ "TSIntrinsicKeyword",
21
+ "TSLiteralType",
22
+ "TSMappedType",
23
+ "TSNamedTupleMember",
24
+ "TSNeverKeyword",
25
+ "TSNullKeyword",
26
+ "TSNumberKeyword",
27
+ "TSObjectKeyword",
28
+ "TSParenthesizedType",
29
+ "TSStringKeyword",
30
+ "TSSymbolKeyword",
31
+ "TSTemplateLiteralType",
32
+ "TSThisType",
33
+ "TSTupleType",
34
+ "TSTypeLiteral",
35
+ "TSTypeOperator",
36
+ "TSTypePredicate",
37
+ "TSTypeQuery",
38
+ "TSTypeReference",
39
+ "TSUndefinedKeyword",
40
+ "TSUnionType",
41
+ "TSUnknownKeyword",
42
+ "TSVoidKeyword",
43
+ ]);
44
+ function isTypeNode(node) {
45
+ return typeNodeKinds.has(node.type);
46
+ }
47
+ function typeReferenceName(type) {
48
+ return type.typeName.type === "Identifier" ? type.typeName.name : null;
49
+ }
50
+ function isInsideTypeAliasDeclaration(node) {
51
+ let current = node.parent;
52
+ while (current !== null && current.type !== "Program") {
53
+ if (current.type === "TSTypeAliasDeclaration") {
54
+ return true;
55
+ }
56
+ current = current.parent;
57
+ }
58
+ return false;
59
+ }
60
+ function isPlainAliasConsumerUse(node, environment) {
61
+ if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) {
62
+ return false;
63
+ }
64
+ const name = typeReferenceName(node);
65
+ return (name !== null &&
66
+ visibleTypeAlias(name, node, environment.typeAliases) !== null &&
67
+ !isInsideTypeAliasDeclaration(node));
68
+ }
69
+ function isInsideTypeParameterConstraint(node) {
70
+ let child = node;
71
+ let parent = node.parent;
72
+ while (parent.type !== "Program") {
73
+ if (parent.type === "TSTypeParameter" && parent.constraint === child) {
74
+ return true;
75
+ }
76
+ child = parent;
77
+ parent = child.parent;
78
+ }
79
+ return false;
80
+ }
81
+ function shouldReportType(node, environment) {
82
+ if (isInsideTypeParameterConstraint(node)) {
83
+ return false;
84
+ }
85
+ if (isPlainAliasConsumerUse(node, environment)) {
86
+ return false;
87
+ }
88
+ if (classifyUnsafeDictionary(node, environment) === null) {
89
+ return false;
90
+ }
91
+ let current = node.parent;
92
+ while (current.type !== "Program") {
93
+ if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null) {
94
+ return false;
95
+ }
96
+ current = current.parent;
97
+ }
98
+ return true;
99
+ }
100
+ export const noUnsafeDictionaryType = defineRule({
101
+ meta: {
102
+ type: "problem",
103
+ docs: {
104
+ description: "Disallow object-dictionary contracts whose direct value type is unknown, any, object, {}, or a union/alias containing one of those escape hatches.",
105
+ },
106
+ messages: {
107
+ 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.",
108
+ },
109
+ },
110
+ createOnce(context) {
111
+ let environment = null;
112
+ const report = (node, value) => {
113
+ context.report({ node, messageId: "unsafeDictionary", data: { value } });
114
+ };
115
+ const reportIfUnsafe = (node) => {
116
+ if (environment === null || !shouldReportType(node, environment)) {
117
+ return;
118
+ }
119
+ const unsafe = classifyUnsafeDictionary(node, environment);
120
+ if (unsafe === null) {
121
+ return;
122
+ }
123
+ report(node, unsafe.unsafeValue);
124
+ };
125
+ return {
126
+ Program(node) {
127
+ environment = createTypeEnvironment(node, context.sourceCode.visitorKeys);
128
+ },
129
+ TSTypeReference: reportIfUnsafe,
130
+ TSTypeLiteral: reportIfUnsafe,
131
+ TSMappedType: reportIfUnsafe,
132
+ TSIndexSignature(node) {
133
+ if (environment === null || node.parent.type === "TSTypeLiteral") {
134
+ return;
135
+ }
136
+ const unsafe = classifyUnsafeDictionaryValue(node.typeAnnotation.typeAnnotation, environment);
137
+ if (unsafe !== null) {
138
+ report(node, unsafe.unsafeValue);
139
+ }
140
+ },
141
+ };
142
+ },
143
+ });
@@ -0,0 +1 @@
1
+ export declare const noWidenThenAssert: import("@oxlint/plugins").Rule;
@@ -0,0 +1,288 @@
1
+ // Adapted from dmmulroy/anti-slop
2
+ import { defineRule } from "@oxlint/plugins";
3
+ const functionBoundaryTypes = new Set([
4
+ "ArrowFunctionExpression",
5
+ "FunctionDeclaration",
6
+ "FunctionExpression",
7
+ "TSDeclareFunction",
8
+ "TSEmptyBodyFunctionExpression",
9
+ ]);
10
+ const objectTypeKinds = new Set([
11
+ "TSArrayType",
12
+ "TSConstructorType",
13
+ "TSFunctionType",
14
+ "TSMappedType",
15
+ "TSObjectKeyword",
16
+ "TSTupleType",
17
+ ]);
18
+ function unwrapExpressionParentheses(expression) {
19
+ let current = expression;
20
+ while (current.type === "ParenthesizedExpression") {
21
+ current = current.expression;
22
+ }
23
+ return current;
24
+ }
25
+ function unwrapTypeParentheses(type) {
26
+ let current = type;
27
+ while (current.type === "TSParenthesizedType") {
28
+ current = current.typeAnnotation;
29
+ }
30
+ return current;
31
+ }
32
+ function typeReferenceName(type) {
33
+ return type.typeName.type === "Identifier" ? type.typeName.name : null;
34
+ }
35
+ function isUnknownOrAnyType(type) {
36
+ const unwrapped = unwrapTypeParentheses(type);
37
+ return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword";
38
+ }
39
+ function isBroadRecordKeyType(type) {
40
+ const unwrapped = unwrapTypeParentheses(type);
41
+ if (unwrapped.type === "TSStringKeyword" ||
42
+ unwrapped.type === "TSNumberKeyword" ||
43
+ unwrapped.type === "TSSymbolKeyword") {
44
+ return true;
45
+ }
46
+ if (unwrapped.type === "TSUnionType") {
47
+ return unwrapped.types.every(isBroadRecordKeyType);
48
+ }
49
+ return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
50
+ }
51
+ function isBroadRecordType(type) {
52
+ const unwrapped = unwrapTypeParentheses(type);
53
+ if (unwrapped.type === "TSTypeReference") {
54
+ if (typeReferenceName(unwrapped) === "Readonly") {
55
+ const [inner] = unwrapped.typeArguments?.params ?? [];
56
+ return inner !== void 0 && isBroadRecordType(inner);
57
+ }
58
+ if (typeReferenceName(unwrapped) !== "Record") {
59
+ return false;
60
+ }
61
+ const parameters = unwrapped.typeArguments?.params ?? [];
62
+ return (parameters.length === 2 &&
63
+ parameters[0] !== void 0 &&
64
+ parameters[1] !== void 0 &&
65
+ isBroadRecordKeyType(parameters[0]) &&
66
+ isUnknownOrAnyType(parameters[1]));
67
+ }
68
+ if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) {
69
+ return false;
70
+ }
71
+ const [member] = unwrapped.members;
72
+ const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
73
+ return (member?.type === "TSIndexSignature" &&
74
+ member.parameters.length === 1 &&
75
+ parameter !== void 0 &&
76
+ isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) &&
77
+ isUnknownOrAnyType(member.typeAnnotation.typeAnnotation));
78
+ }
79
+ function broadTypeKind(type) {
80
+ const unwrapped = unwrapTypeParentheses(type);
81
+ if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") {
82
+ return "top";
83
+ }
84
+ if (unwrapped.type === "TSObjectKeyword") {
85
+ return "object";
86
+ }
87
+ return isBroadRecordType(unwrapped) ? "record" : null;
88
+ }
89
+ function assertedExpression(node) {
90
+ return unwrapExpressionParentheses(node.expression);
91
+ }
92
+ function assertionFromExpression(expression) {
93
+ const unwrapped = unwrapExpressionParentheses(expression);
94
+ return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion"
95
+ ? unwrapped
96
+ : null;
97
+ }
98
+ function normalizedTypeText(sourceText, type) {
99
+ return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, "");
100
+ }
101
+ function typesHaveSameSyntax(sourceText, left, right) {
102
+ return (left !== null &&
103
+ normalizedTypeText(sourceText, unwrapTypeParentheses(left)) ===
104
+ normalizedTypeText(sourceText, unwrapTypeParentheses(right)));
105
+ }
106
+ function isDefinitelyObjectType(type) {
107
+ const unwrapped = unwrapTypeParentheses(type);
108
+ if (unwrapped.type === "TSTypeLiteral") {
109
+ return unwrapped.members.length > 0;
110
+ }
111
+ if (unwrapped.type === "TSIntersectionType") {
112
+ return unwrapped.types.every(isDefinitelyObjectType);
113
+ }
114
+ if (unwrapped.type === "TSTypeOperator") {
115
+ return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation);
116
+ }
117
+ return objectTypeKinds.has(unwrapped.type);
118
+ }
119
+ function isDefinitelyNarrowerRecordType(type) {
120
+ const unwrapped = unwrapTypeParentheses(type);
121
+ if (unwrapped.type === "TSTypeLiteral") {
122
+ return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
123
+ }
124
+ if (unwrapped.type !== "TSTypeReference") {
125
+ return false;
126
+ }
127
+ if (typeReferenceName(unwrapped) === "Readonly") {
128
+ const [inner] = unwrapped.typeArguments?.params ?? [];
129
+ return inner !== void 0 && isDefinitelyNarrowerRecordType(inner);
130
+ }
131
+ if (typeReferenceName(unwrapped) !== "Record") {
132
+ return false;
133
+ }
134
+ const parameters = unwrapped.typeArguments?.params ?? [];
135
+ return parameters.length === 2 && parameters[1] !== void 0 && !isUnknownOrAnyType(parameters[1]);
136
+ }
137
+ function functionBoundary(node) {
138
+ let current = node.parent;
139
+ while (current !== null && current.type !== "Program") {
140
+ if (functionBoundaryTypes.has(current.type)) {
141
+ return current;
142
+ }
143
+ current = current.parent;
144
+ }
145
+ return null;
146
+ }
147
+ function resolvedVariableForIdentifier(scopes, identifier) {
148
+ for (const scope of scopes) {
149
+ const reference = scope.references.find((candidate) => candidate.identifier.start === identifier.start &&
150
+ candidate.identifier.end === identifier.end);
151
+ if (reference !== void 0) {
152
+ return reference.resolved;
153
+ }
154
+ }
155
+ return null;
156
+ }
157
+ function variableDeclarator(variable) {
158
+ for (const definition of variable.defs) {
159
+ if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") {
160
+ return definition.node;
161
+ }
162
+ }
163
+ return null;
164
+ }
165
+ function knownValueEvidence(expression, scopes, boundary, visitedVariables) {
166
+ const unwrapped = unwrapExpressionParentheses(expression);
167
+ if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
168
+ if (broadTypeKind(unwrapped.typeAnnotation) !== null) {
169
+ return null;
170
+ }
171
+ return { type: unwrapped.typeAnnotation };
172
+ }
173
+ if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") {
174
+ return { type: null };
175
+ }
176
+ if (unwrapped.type === "ArrayExpression" ||
177
+ unwrapped.type === "ArrowFunctionExpression" ||
178
+ unwrapped.type === "ClassExpression" ||
179
+ unwrapped.type === "FunctionExpression" ||
180
+ unwrapped.type === "NewExpression" ||
181
+ unwrapped.type === "ObjectExpression") {
182
+ return { type: null };
183
+ }
184
+ if (unwrapped.type !== "Identifier") {
185
+ return null;
186
+ }
187
+ const variable = resolvedVariableForIdentifier(scopes, unwrapped);
188
+ if (variable === null || visitedVariables.has(variable)) {
189
+ return null;
190
+ }
191
+ const annotatedIdentifier = variable.identifiers.find((identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== void 0);
192
+ const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
193
+ if (annotation !== void 0 && annotatedIdentifier !== void 0) {
194
+ if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) {
195
+ return null;
196
+ }
197
+ return { type: annotation };
198
+ }
199
+ const declarator = variableDeclarator(variable);
200
+ if (declarator === null ||
201
+ declarator.parent.type !== "VariableDeclaration" ||
202
+ declarator.parent.kind !== "const" ||
203
+ declarator.init === null ||
204
+ variable.references.some((reference) => reference.isWrite() && !reference.init) ||
205
+ functionBoundary(declarator) !== boundary) {
206
+ return null;
207
+ }
208
+ return knownValueEvidence(declarator.init, scopes, boundary, new Set([...visitedVariables, variable]));
209
+ }
210
+ function widenedBinding(variable, scopes) {
211
+ const declarator = variableDeclarator(variable);
212
+ if (declarator === null ||
213
+ declarator.parent.type !== "VariableDeclaration" ||
214
+ declarator.parent.kind !== "const" ||
215
+ declarator.id.type !== "Identifier" ||
216
+ declarator.init === null ||
217
+ variable.references.some((reference) => reference.isWrite() && !reference.init)) {
218
+ return null;
219
+ }
220
+ const boundary = functionBoundary(declarator);
221
+ const declaredType = declarator.id.typeAnnotation?.typeAnnotation;
222
+ const initializerAssertion = assertionFromExpression(declarator.init);
223
+ const initializerBroadKind = initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
224
+ const declaredBroadKind = declaredType === void 0 ? null : broadTypeKind(declaredType);
225
+ const broadKind = declaredBroadKind ?? initializerBroadKind;
226
+ if (broadKind === null) {
227
+ return null;
228
+ }
229
+ const originalExpression = initializerAssertion !== null && initializerBroadKind !== null
230
+ ? assertedExpression(initializerAssertion)
231
+ : declarator.init;
232
+ const evidence = knownValueEvidence(originalExpression, scopes, boundary, new Set([variable]));
233
+ return evidence === null ? null : { broadKind, evidence, declaredAt: declarator.end, boundary };
234
+ }
235
+ function assertionIsNarrower(sourceText, broadKind, evidence, assertedType) {
236
+ if (broadTypeKind(assertedType) !== null) {
237
+ return false;
238
+ }
239
+ if (broadKind === "top") {
240
+ return true;
241
+ }
242
+ if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) {
243
+ return true;
244
+ }
245
+ if (broadKind === "object") {
246
+ return isDefinitelyObjectType(assertedType);
247
+ }
248
+ return isDefinitelyNarrowerRecordType(assertedType);
249
+ }
250
+ export const noWidenThenAssert = defineRule({
251
+ meta: {
252
+ type: "problem",
253
+ docs: {
254
+ description: "Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type.",
255
+ },
256
+ messages: {
257
+ 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.',
258
+ },
259
+ },
260
+ createOnce(context) {
261
+ let scopes = [];
262
+ const checkAssertion = (node) => {
263
+ const expression = assertedExpression(node);
264
+ if (expression.type !== "Identifier") {
265
+ return;
266
+ }
267
+ const variable = resolvedVariableForIdentifier(scopes, expression);
268
+ if (variable === null) {
269
+ return;
270
+ }
271
+ const widened = widenedBinding(variable, scopes);
272
+ if (widened === null ||
273
+ node.start <= widened.declaredAt ||
274
+ functionBoundary(node) !== widened.boundary ||
275
+ !assertionIsNarrower(context.sourceCode.text, widened.broadKind, widened.evidence, node.typeAnnotation)) {
276
+ return;
277
+ }
278
+ context.report({ node, messageId: "widenThenAssert", data: { name: expression.name } });
279
+ };
280
+ return {
281
+ Program() {
282
+ scopes = context.sourceCode.scopeManager.scopes;
283
+ },
284
+ TSAsExpression: checkAssertion,
285
+ TSTypeAssertion: checkAssertion,
286
+ };
287
+ },
288
+ });