oxlint-plugin-effect 0.12.1 → 0.14.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 (47) hide show
  1. package/README.md +25 -14
  2. package/THIRD_PARTY_NOTICES.md +4 -2
  3. package/dist/presets/recommended.d.ts +9 -0
  4. package/dist/presets/recommended.js +10 -1
  5. package/dist/rules/_anti-slop-array-method.js +70 -0
  6. package/dist/rules/_anti-slop-dictionary-types.js +58 -53
  7. package/dist/rules/_anti-slop-function-parameters.js +27 -0
  8. package/dist/rules/_anti-slop-lexical-type-parameters.js +33 -0
  9. package/dist/rules/_anti-slop-reflect-method.js +21 -0
  10. package/dist/rules/_anti-slop-scope.js +13 -0
  11. package/dist/rules/_anti-slop-type-alias-resolution.js +146 -0
  12. package/dist/rules/_function-metrics.js +31 -31
  13. package/dist/rules/_test-files.js +8 -0
  14. package/dist/rules/index.d.ts +10 -1
  15. package/dist/rules/index.js +19 -1
  16. package/dist/rules/no-array-filter-map.d.ts +7 -0
  17. package/dist/rules/no-array-filter-map.js +39 -0
  18. package/dist/rules/no-as.js +12 -6
  19. package/dist/rules/no-conditional-empty-object-spread.d.ts +1 -1
  20. package/dist/rules/no-conditional-empty-object-spread.js +8 -51
  21. package/dist/rules/no-eager-acquire.d.ts +6 -0
  22. package/dist/rules/no-eager-acquire.js +92 -0
  23. package/dist/rules/no-effect-run-in-tests.d.ts +6 -0
  24. package/dist/rules/no-effect-run-in-tests.js +57 -0
  25. package/dist/rules/no-known-value-widening.js +6 -14
  26. package/dist/rules/no-module-level-mutable-state.d.ts +6 -0
  27. package/dist/rules/no-module-level-mutable-state.js +51 -0
  28. package/dist/rules/no-object-parameters.js +11 -27
  29. package/dist/rules/no-reduce-accumulator-copy.d.ts +7 -0
  30. package/dist/rules/no-reduce-accumulator-copy.js +94 -0
  31. package/dist/rules/no-reflect-apply.d.ts +7 -0
  32. package/dist/rules/no-reflect-apply.js +25 -0
  33. package/dist/rules/no-reflect-get.d.ts +7 -0
  34. package/dist/rules/no-reflect-get.js +25 -0
  35. package/dist/rules/no-runtime-typeof.js +14 -1
  36. package/dist/rules/no-shape-in-symbol-names.js +7 -1
  37. package/dist/rules/no-unknown-parameters.d.ts +1 -1
  38. package/dist/rules/no-unknown-parameters.js +23 -16
  39. package/dist/rules/no-unknown-returns.d.ts +7 -0
  40. package/dist/rules/no-unknown-returns.js +73 -0
  41. package/dist/rules/no-unknown-type-aliases.js +17 -27
  42. package/dist/rules/no-unsafe-dictionary-type.js +54 -3
  43. package/dist/rules/no-widen-then-assert.js +1 -1
  44. package/dist/rules/require-suppression-reason.d.ts +6 -0
  45. package/dist/rules/require-suppression-reason.js +87 -0
  46. package/dist/vendor/effect-oxlint/Diagnostic.js +1 -1
  47. package/package.json +5 -4
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-effect-run-in-tests.d.ts
4
+ declare const noEffectRunInTests: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { noEffectRunInTests };
@@ -0,0 +1,57 @@
1
+ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
6
+ import { isTestFile, skipFile } from "./_test-files.js";
7
+ import * as Effect from "effect/Effect";
8
+ //#region src/rules/no-effect-run-in-tests.ts
9
+ const effectRunners = [
10
+ "runCallback",
11
+ "runCallbackWith",
12
+ "runFork",
13
+ "runForkWith",
14
+ "runPromise",
15
+ "runPromiseExit",
16
+ "runPromiseExitWith",
17
+ "runPromiseWith",
18
+ "runSync",
19
+ "runSyncExit",
20
+ "runSyncExitWith",
21
+ "runSyncWith"
22
+ ];
23
+ const noEffectRunInTests = define({
24
+ name: "no-effect-run-in-tests",
25
+ meta: meta({
26
+ type: "problem",
27
+ description: "Run Effects in tests through it.effect or it.layer instead of Effect.run* or ManagedRuntime.make."
28
+ }),
29
+ create: function* () {
30
+ const ctx = yield* RuleContext;
31
+ if (!isTestFile(ctx.filename)) return skipFile;
32
+ const effectNamespaces = new Set(["Effect"]);
33
+ const managedRuntimeNamespaces = new Set(["ManagedRuntime"]);
34
+ const runnerName = (node) => {
35
+ if (isStaticMember(node, visibleNamespaces(ctx, node, managedRuntimeNamespaces), "make")) return "ManagedRuntime.make";
36
+ const effects = visibleNamespaces(ctx, node, effectNamespaces);
37
+ return effectRunners.find((runner) => isStaticMember(node, effects, runner));
38
+ };
39
+ return {
40
+ ImportDeclaration: (node) => {
41
+ for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
42
+ for (const name of importedNamespaces(node, "ManagedRuntime", "effect/ManagedRuntime")) managedRuntimeNamespaces.add(name);
43
+ return Effect.void;
44
+ },
45
+ MemberExpression: (node) => {
46
+ const runner = runnerName(node);
47
+ if (runner === void 0) return Effect.void;
48
+ return ctx.report(make({
49
+ node,
50
+ message: `Do not run Effects by hand in tests (${runner}). Use the test runner's Effect integration: it.effect(...) or it.layer(layer)(...) from @effect/vitest or effect-bun-test.`
51
+ }));
52
+ }
53
+ };
54
+ }
55
+ });
56
+ //#endregion
57
+ export { noEffectRunInTests };
@@ -3,26 +3,18 @@ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
5
  import { classifyWideningTarget, createTypeEnvironment, isKnownEvidenceExpression } from "./_anti-slop-dictionary-types.js";
6
+ import { resolveVariable } from "./_anti-slop-scope.js";
6
7
  import * as Effect from "effect/Effect";
7
8
  //#region src/rules/no-known-value-widening.ts
8
9
  /**
9
10
  * Ported from dmmulroy/anti-slop at
10
- * b5d2288db1f00469a1d5f2e3b0e265e5a5676fd0.
11
+ * c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b.
11
12
  */
12
13
  function unwrapExpression(expression) {
13
14
  let current = expression;
14
15
  while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression") current = current.expression;
15
16
  return current;
16
17
  }
17
- function resolveVariable(sourceCode, identifier) {
18
- let scope = sourceCode.getScope(identifier);
19
- while (scope !== null) {
20
- const variable = scope.set.get(identifier.name);
21
- if (variable !== void 0) return variable;
22
- scope = scope.upper;
23
- }
24
- return null;
25
- }
26
18
  function variableDeclarator(variable) {
27
19
  if (variable.defs.length !== 1) return null;
28
20
  const [definition] = variable.defs;
@@ -87,9 +79,9 @@ const noKnownValueWidening = define({
87
79
  create: function* () {
88
80
  const context = yield* RuleContext;
89
81
  let environment = null;
90
- const reportFlow = (expression, destination, subject, options = {}) => {
82
+ const reportFlow = (expression, destination, subject) => {
91
83
  if (destination === null) return Effect.void;
92
- if (options.allowEmptyDictionaryAccumulator === true && isDictionaryAccumulatorTarget(destination) && isEmptyObjectExpression(expression)) return Effect.void;
84
+ if (isDictionaryAccumulatorTarget(destination) && isEmptyObjectExpression(expression)) return Effect.void;
93
85
  if (!hasKnownEvidence(context.sourceCode, expression)) return Effect.void;
94
86
  return context.report(fromId({
95
87
  node: expression,
@@ -103,12 +95,12 @@ const noKnownValueWidening = define({
103
95
  const targetFromAnnotation = (annotation) => environment === null ? null : annotationTarget(annotation, environment);
104
96
  return {
105
97
  Program: (node) => {
106
- environment = createTypeEnvironment(node);
98
+ environment = createTypeEnvironment(node, context.sourceCode.visitorKeys);
107
99
  return Effect.void;
108
100
  },
109
101
  VariableDeclarator: (node) => {
110
102
  if (node.init === null || node.id.type !== "Identifier") return Effect.void;
111
- return reportFlow(node.init, targetFromAnnotation(node.id.typeAnnotation), `binding \`${node.id.name}\``, { allowEmptyDictionaryAccumulator: true });
103
+ return reportFlow(node.init, targetFromAnnotation(node.id.typeAnnotation), `binding \`${node.id.name}\``);
112
104
  },
113
105
  PropertyDefinition: (node) => {
114
106
  if (node.value === null) return Effect.void;
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-module-level-mutable-state.d.ts
4
+ declare const noModuleLevelMutableState: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { noModuleLevelMutableState };
@@ -0,0 +1,51 @@
1
+ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { isTestFile, skipFile } from "./_test-files.js";
6
+ import * as Effect from "effect/Effect";
7
+ //#region src/rules/no-module-level-mutable-state.ts
8
+ /** Nodes that start a new `var` scope, so a `var` below them is not module state. */
9
+ const functionScopeBoundaries = new Set([
10
+ "ArrowFunctionExpression",
11
+ "FunctionDeclaration",
12
+ "FunctionExpression",
13
+ "StaticBlock",
14
+ "TSDeclareFunction",
15
+ "TSModuleBlock"
16
+ ]);
17
+ const isModuleScopedVar = (node) => {
18
+ let current = node.parent;
19
+ while (current !== null && current.type !== "Program") {
20
+ if (functionScopeBoundaries.has(current.type)) return false;
21
+ current = current.parent;
22
+ }
23
+ return current !== null;
24
+ };
25
+ const noModuleLevelMutableState = define({
26
+ name: "no-module-level-mutable-state",
27
+ meta: meta({
28
+ type: "problem",
29
+ description: "Keep state out of module-level let and var bindings."
30
+ }),
31
+ create: function* () {
32
+ const ctx = yield* RuleContext;
33
+ if (isTestFile(ctx.filename)) return skipFile;
34
+ const isModuleScoped = (node) => {
35
+ if (node.kind === "var") return isModuleScopedVar(node);
36
+ const scope = ctx.sourceCode.getScope(node);
37
+ return scope.type === "module" || scope.type === "global";
38
+ };
39
+ return { VariableDeclaration: (node) => {
40
+ if (node.declare === true) return Effect.void;
41
+ if (node.kind !== "let" && node.kind !== "var") return Effect.void;
42
+ if (!isModuleScoped(node)) return Effect.void;
43
+ return ctx.report(make({
44
+ node,
45
+ message: `Module-level ${node.kind} is shared by every request a server process or Worker isolate handles. Keep the state in a Ref built by a Layer, or in the service that owns it.`
46
+ }));
47
+ } };
48
+ }
49
+ });
50
+ //#endregion
51
+ export { noModuleLevelMutableState };
@@ -2,17 +2,10 @@ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
2
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
+ import { createTypeAliasEnvironment, resolvedTypeMatches } from "./_anti-slop-type-alias-resolution.js";
6
+ import { functionParameterBindingName, functionParameterTypeAnnotation } from "./_anti-slop-function-parameters.js";
5
7
  import * as Effect from "effect/Effect";
6
8
  //#region src/rules/no-object-parameters.ts
7
- function parameterAnnotation(parameter) {
8
- if (parameter.type === "TSParameterProperty") return parameterAnnotation(parameter.parameter);
9
- if (parameter.type === "RestElement") return parameter.typeAnnotation ?? parameterAnnotation(parameter.argument);
10
- if (parameter.type === "AssignmentPattern") return parameter.typeAnnotation ?? parameter.left.typeAnnotation;
11
- return parameter.typeAnnotation;
12
- }
13
- function parameterName(parameter, sourceCode) {
14
- return parameter.type === "Identifier" ? parameter.name : sourceCode.getText(parameter).replace(/\s*:\s*object\s*$/u, "");
15
- }
16
9
  /** Ban the broad object type on function inputs, including local aliases to object. */
17
10
  const noObjectParameters = define({
18
11
  name: "no-object-parameters",
@@ -23,34 +16,25 @@ const noObjectParameters = define({
23
16
  }),
24
17
  create: function* () {
25
18
  const context = yield* RuleContext;
26
- const aliases = /* @__PURE__ */ new Map();
27
- const resolvesToObject = (type, visited = /* @__PURE__ */ new Set()) => {
28
- if (type.type === "TSObjectKeyword") return true;
29
- if (type.type === "TSParenthesizedType") return resolvesToObject(type.typeAnnotation, visited);
30
- if (type.type === "TSUnionType") return type.types.some((member) => resolvesToObject(member, visited));
31
- if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier" || type.typeArguments !== null && type.typeArguments !== void 0 && type.typeArguments.params.length > 0 || visited.has(type.typeName.name)) return false;
32
- const alias = aliases.get(type.typeName.name);
33
- if (alias === void 0) return false;
34
- const nextVisited = new Set(visited);
35
- nextVisited.add(type.typeName.name);
36
- return resolvesToObject(alias, nextVisited);
37
- };
19
+ let environment = null;
20
+ const resolvesToObject = (type) => environment !== null && resolvedTypeMatches(type, environment, (resolved, matches) => {
21
+ if (resolved.type === "TSObjectKeyword") return true;
22
+ if (resolved.type === "TSParenthesizedType") return matches(resolved.typeAnnotation);
23
+ return resolved.type === "TSUnionType" && resolved.types.some(matches);
24
+ });
38
25
  const checkParameters = (node) => Effect.forEach(node.params, (parameter) => {
39
- const annotation = parameterAnnotation(parameter);
26
+ const annotation = functionParameterTypeAnnotation(parameter);
40
27
  if (annotation === null || annotation === void 0) return Effect.void;
41
28
  if (!resolvesToObject(annotation.typeAnnotation)) return Effect.void;
42
29
  return context.report(fromId({
43
30
  node: annotation.typeAnnotation,
44
31
  messageId: "objectParameter",
45
- data: { parameter: parameterName(parameter, context.sourceCode) }
32
+ data: { parameter: functionParameterBindingName(parameter, context.sourceCode) }
46
33
  }));
47
34
  }, { discard: true });
48
35
  return {
49
36
  Program: (node) => {
50
- for (const statement of node.body) {
51
- const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
52
- if (declaration?.type === "TSTypeAliasDeclaration" && (declaration.typeParameters === null || declaration.typeParameters === void 0)) aliases.set(declaration.id.name, declaration.typeAnnotation);
53
- }
37
+ environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
54
38
  return Effect.void;
55
39
  },
56
40
  ArrowFunctionExpression: checkParameters,
@@ -0,0 +1,7 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-reduce-accumulator-copy.d.ts
4
+ /** Reject non-spread copies of reducer accumulators; pair with oxc/no-accumulating-spread. */
5
+ declare const noReduceAccumulatorCopy: _$_oxlint_plugins0.CreateRule;
6
+ //#endregion
7
+ export { noReduceAccumulatorCopy };
@@ -0,0 +1,94 @@
1
+ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { arrayMethodTarget, isKnownArrayExpression, resolveArrayBinding, unwrapArrayExpression } from "./_anti-slop-array-method.js";
6
+ import * as Effect from "effect/Effect";
7
+ //#region src/rules/no-reduce-accumulator-copy.ts
8
+ const arrayCopyMethods = new Set([
9
+ "concat",
10
+ "slice",
11
+ "toSpliced",
12
+ "toSorted",
13
+ "toReversed",
14
+ "with"
15
+ ]);
16
+ function enclosingReducer(node) {
17
+ let parent = node.parent;
18
+ while (parent !== null) {
19
+ if (parent.type === "FunctionDeclaration") return null;
20
+ if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") {
21
+ const callback = parent;
22
+ let owner = callback.parent;
23
+ while (owner !== null && unwrapArrayExpression(owner) === callback) owner = owner.parent;
24
+ if (owner?.type !== "CallExpression") return null;
25
+ const method = arrayMethodTarget(owner.callee);
26
+ const firstArgument = owner.arguments[0];
27
+ if (method === null || method.name !== "reduce" && method.name !== "reduceRight" || owner.arguments.length > 2 || firstArgument === void 0 || unwrapArrayExpression(firstArgument) !== callback) return null;
28
+ const firstParameter = callback.params[0];
29
+ const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter;
30
+ if (accumulator?.type !== "Identifier") return null;
31
+ return {
32
+ callback,
33
+ accumulator,
34
+ initialValue: owner.arguments[1]
35
+ };
36
+ }
37
+ parent = parent.parent;
38
+ }
39
+ return null;
40
+ }
41
+ function referencesAccumulator(sourceCode, node, accumulator, visited = /* @__PURE__ */ new Set()) {
42
+ const variable = resolveArrayBinding(sourceCode, node);
43
+ if (variable === null || visited.has(variable)) return false;
44
+ if (variable === accumulator) return true;
45
+ visited.add(variable);
46
+ if (variable.references.some((reference) => reference.isWrite() && !reference.init)) return false;
47
+ for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator" && definition.node.id.type === "Identifier" && definition.node.init !== null && definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const") return referencesAccumulator(sourceCode, definition.node.init, accumulator, visited);
48
+ return false;
49
+ }
50
+ function isGlobalCopyOwner(sourceCode, node, name) {
51
+ const owner = unwrapArrayExpression(node);
52
+ if (owner.type !== "Identifier" || owner.name !== name) return false;
53
+ const variable = resolveArrayBinding(sourceCode, owner);
54
+ return variable === null || variable.defs.length === 0;
55
+ }
56
+ /** Reject non-spread copies of reducer accumulators; pair with oxc/no-accumulating-spread. */
57
+ const noReduceAccumulatorCopy = define({
58
+ name: "no-reduce-accumulator-copy",
59
+ meta: meta({
60
+ type: "problem",
61
+ description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods.",
62
+ messages: { accumulatorCopy: "Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline/flatMap." }
63
+ }),
64
+ create: function* () {
65
+ const context = yield* RuleContext;
66
+ const { sourceCode } = context;
67
+ const copiesAccumulator = (node) => {
68
+ const method = arrayMethodTarget(node.callee);
69
+ if (method === null) return false;
70
+ const reducer = enclosingReducer(node);
71
+ if (reducer === null) return false;
72
+ const accumulator = sourceCode.getDeclaredVariables(reducer.callback).find((variable) => variable.identifiers.some((identifier) => identifier.start === reducer.accumulator.start));
73
+ if (accumulator === void 0) return false;
74
+ const isAccumulator = (expression) => referencesAccumulator(sourceCode, expression, accumulator);
75
+ if (method.name === "assign" && isGlobalCopyOwner(sourceCode, method.object, "Object")) {
76
+ const target = node.arguments[0];
77
+ return target !== void 0 && unwrapArrayExpression(target).type === "ObjectExpression" && node.arguments.slice(1).some(isAccumulator);
78
+ }
79
+ if (method.name === "from" && isGlobalCopyOwner(sourceCode, method.object, "Array")) {
80
+ const source = node.arguments[0];
81
+ return source !== void 0 && isAccumulator(source);
82
+ }
83
+ if (!arrayCopyMethods.has(method.name)) return false;
84
+ const initialValue = reducer.initialValue;
85
+ return initialValue !== void 0 && isKnownArrayExpression(sourceCode, initialValue) && isAccumulator(method.object);
86
+ };
87
+ return { CallExpression: (node) => copiesAccumulator(node) ? context.report(fromId({
88
+ node,
89
+ messageId: "accumulatorCopy"
90
+ })) : Effect.void };
91
+ }
92
+ });
93
+ //#endregion
94
+ export { noReduceAccumulatorCopy };
@@ -0,0 +1,7 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-reflect-apply.d.ts
4
+ /** Ban Reflect.apply, which bypasses ordinary typed function calls. */
5
+ declare const noReflectApply: _$_oxlint_plugins0.CreateRule;
6
+ //#endregion
7
+ export { noReflectApply };
@@ -0,0 +1,25 @@
1
+ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { isGlobalReflectMethodCall } from "./_anti-slop-reflect-method.js";
6
+ import * as Effect from "effect/Effect";
7
+ //#region src/rules/no-reflect-apply.ts
8
+ /** Ban Reflect.apply, which bypasses ordinary typed function calls. */
9
+ const noReflectApply = define({
10
+ name: "no-reflect-apply",
11
+ meta: meta({
12
+ type: "problem",
13
+ description: "Disallow Reflect.apply; call typed functions directly or model dynamic dispatch behind an interface.",
14
+ messages: { reflectApply: "Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named interface." }
15
+ }),
16
+ create: function* () {
17
+ const context = yield* RuleContext;
18
+ return { CallExpression: (node) => node.callee.type !== "Super" && isGlobalReflectMethodCall(context.sourceCode, node.callee, "apply") ? context.report(fromId({
19
+ node,
20
+ messageId: "reflectApply"
21
+ })) : Effect.void };
22
+ }
23
+ });
24
+ //#endregion
25
+ export { noReflectApply };
@@ -0,0 +1,7 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-reflect-get.d.ts
4
+ /** Ban Reflect.get, which bypasses ordinary property access and useful type evidence. */
5
+ declare const noReflectGet: _$_oxlint_plugins0.CreateRule;
6
+ //#endregion
7
+ export { noReflectGet };
@@ -0,0 +1,25 @@
1
+ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { isGlobalReflectMethodCall } from "./_anti-slop-reflect-method.js";
6
+ import * as Effect from "effect/Effect";
7
+ //#region src/rules/no-reflect-get.ts
8
+ /** Ban Reflect.get, which bypasses ordinary property access and useful type evidence. */
9
+ const noReflectGet = define({
10
+ name: "no-reflect-get",
11
+ meta: meta({
12
+ type: "problem",
13
+ description: "Disallow Reflect.get; use typed property access or parse dynamic input into a domain type.",
14
+ messages: { reflectGet: "Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type before reading it." }
15
+ }),
16
+ create: function* () {
17
+ const context = yield* RuleContext;
18
+ return { CallExpression: (node) => node.callee.type !== "Super" && isGlobalReflectMethodCall(context.sourceCode, node.callee, "get") ? context.report(fromId({
19
+ node,
20
+ messageId: "reflectGet"
21
+ })) : Effect.void };
22
+ }
23
+ });
24
+ //#endregion
25
+ export { noReflectGet };
@@ -4,6 +4,19 @@ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
5
  import * as Effect from "effect/Effect";
6
6
  //#region src/rules/no-runtime-typeof.ts
7
+ /** Whether `typeof` probes for a possibly absent binding, which only `typeof` can do safely. */
8
+ function isExistenceProbe(node) {
9
+ const parent = node.parent;
10
+ if (parent?.type !== "BinaryExpression") return false;
11
+ if (![
12
+ "===",
13
+ "!==",
14
+ "==",
15
+ "!="
16
+ ].includes(parent.operator)) return false;
17
+ const other = parent.left === node ? parent.right : parent.left;
18
+ return other.type === "Literal" && other.value === "undefined";
19
+ }
7
20
  /** Disallow runtime typeof checks that narrow unparsed values instead of decoding them. */
8
21
  const noRuntimeTypeof = define({
9
22
  name: "no-runtime-typeof",
@@ -14,7 +27,7 @@ const noRuntimeTypeof = define({
14
27
  }),
15
28
  create: function* () {
16
29
  const context = yield* RuleContext;
17
- return { UnaryExpression: (node) => node.operator === "typeof" ? context.report(fromId({
30
+ return { UnaryExpression: (node) => node.operator === "typeof" && !isExistenceProbe(node) ? context.report(fromId({
18
31
  node,
19
32
  messageId: "runtimeTypeof"
20
33
  })) : Effect.void };
@@ -8,6 +8,12 @@ const forbiddenSymbolName = "shape";
8
8
  function containsForbiddenSymbolName(name) {
9
9
  return name.toLowerCase().includes(forbiddenSymbolName);
10
10
  }
11
+ /** Return whether an identifier names a statically accessed member owned by another value. */
12
+ function isBorrowedMemberName(node) {
13
+ const parent = node.parent;
14
+ if (parent === null || parent.type !== "MemberExpression") return false;
15
+ return parent.property === node && !parent.computed;
16
+ }
11
17
  /** Ban the case-insensitive substring "shape" in every JavaScript and TypeScript symbol name. */
12
18
  const noShapeInSymbolNames = define({
13
19
  name: "no-shape-in-symbol-names",
@@ -18,7 +24,7 @@ const noShapeInSymbolNames = define({
18
24
  }),
19
25
  create: function* () {
20
26
  const context = yield* RuleContext;
21
- const reportForbiddenSymbolName = (node) => containsForbiddenSymbolName(node.name) ? context.report(fromId({
27
+ const reportForbiddenSymbolName = (node) => containsForbiddenSymbolName(node.name) && !isBorrowedMemberName(node) ? context.report(fromId({
22
28
  node,
23
29
  messageId: "forbiddenSymbolName",
24
30
  data: { name: node.name }
@@ -1,7 +1,7 @@
1
1
  import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
2
 
3
3
  //#region src/rules/no-unknown-parameters.d.ts
4
- /** Disallow unknown inputs except explicitly named error-cause enrichment. */
4
+ /** Disallow unknown inputs except error-cause enrichment, type guards, and rejection handlers. */
5
5
  declare const noUnknownParameters: _$_oxlint_plugins0.CreateRule;
6
6
  //#endregion
7
7
  export { noUnknownParameters };
@@ -2,35 +2,42 @@ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
2
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
+ import { containsUnknownType, functionParameterBindingName, functionParameterTypeAnnotation } from "./_anti-slop-function-parameters.js";
5
6
  import * as Effect from "effect/Effect";
6
7
  //#region src/rules/no-unknown-parameters.ts
7
- function parameterAnnotation(parameter) {
8
- if (parameter.type === "TSParameterProperty") return parameterAnnotation(parameter.parameter);
9
- if (parameter.type === "RestElement") return parameter.typeAnnotation ?? parameterAnnotation(parameter.argument);
10
- if (parameter.type === "AssignmentPattern") return parameter.typeAnnotation ?? parameter.left.typeAnnotation;
11
- return parameter.typeAnnotation;
8
+ function isTypePredicateSubject(owner, parameterName) {
9
+ const predicate = owner.returnType?.typeAnnotation;
10
+ return predicate?.type === "TSTypePredicate" && predicate.parameterName.type === "Identifier" && predicate.parameterName.name === parameterName;
12
11
  }
13
- function parameterName(parameter, sourceText) {
14
- if (parameter.type === "TSParameterProperty") return parameterName(parameter.parameter, sourceText);
15
- if (parameter.type === "AssignmentPattern") return parameterName(parameter.left, sourceText);
16
- if (parameter.type === "RestElement") return parameterName(parameter.argument, sourceText);
17
- return parameter.type === "Identifier" ? parameter.name : sourceText.replace(/\s*:\s*unknown\s*$/u, "");
12
+ /**
13
+ * Whether the function is a promise rejection handler: the argument to `.catch`
14
+ * or the second argument to `.then`. Its first parameter is the function form of
15
+ * a `catch` clause binding and receives `unknown` by nature.
16
+ */
17
+ function isRejectionHandler(owner) {
18
+ if (owner.type !== "ArrowFunctionExpression" && owner.type !== "FunctionExpression") return false;
19
+ const call = owner.parent;
20
+ if (call?.type !== "CallExpression" || call.callee.type !== "MemberExpression" || call.callee.computed || call.callee.property.type !== "Identifier") return false;
21
+ const position = call.arguments.indexOf(owner);
22
+ const method = call.callee.property.name;
23
+ return method === "catch" && position === 0 || method === "then" && position === 1;
18
24
  }
19
- /** Disallow unknown inputs except explicitly named error-cause enrichment. */
25
+ /** Disallow unknown inputs except error-cause enrichment, type guards, and rejection handlers. */
20
26
  const noUnknownParameters = define({
21
27
  name: "no-unknown-parameters",
22
28
  meta: meta({
23
29
  type: "problem",
24
- description: "Disallow explicitly unknown function parameters except `cause`; decode unknown input at its I/O boundary instead.",
30
+ description: "Disallow explicitly unknown function parameters except `cause`, type-predicate subjects, and the reason a promise rejection handler receives; decode unknown input at its I/O boundary instead.",
25
31
  messages: { unknownParameter: "Parameter `{{parameter}}` accepts `unknown` without establishing its contract. Define the expected schema or parser so the value becomes a strongly typed domain type at the earliest possible point, as close as possible to the I/O boundary where the data originated." }
26
32
  }),
27
33
  create: function* () {
28
34
  const context = yield* RuleContext;
29
35
  const checkParameters = (node) => Effect.forEach(node.params, (parameter) => {
30
- const annotation = parameterAnnotation(parameter);
31
- if (annotation?.typeAnnotation.type !== "TSUnknownKeyword") return Effect.void;
32
- const name = parameterName(parameter, context.sourceCode.getText(parameter));
33
- if (name === "cause") return Effect.void;
36
+ const annotation = functionParameterTypeAnnotation(parameter);
37
+ if (annotation === null || annotation === void 0) return Effect.void;
38
+ if (!containsUnknownType(annotation.typeAnnotation)) return Effect.void;
39
+ const name = functionParameterBindingName(parameter, context.sourceCode);
40
+ if (name === "cause" || isTypePredicateSubject(node, name) || parameter === node.params[0] && isRejectionHandler(node)) return Effect.void;
34
41
  return context.report(fromId({
35
42
  node: annotation.typeAnnotation,
36
43
  messageId: "unknownParameter",
@@ -0,0 +1,7 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-unknown-returns.d.ts
4
+ /** Ban function contracts that return unknown instead of a parsed domain type. */
5
+ declare const noUnknownReturns: _$_oxlint_plugins0.CreateRule;
6
+ //#endregion
7
+ export { noUnknownReturns };
@@ -0,0 +1,73 @@
1
+ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { createTypeAliasEnvironment, resolvedTypeMatches } from "./_anti-slop-type-alias-resolution.js";
6
+ import * as Effect from "effect/Effect";
7
+ //#region src/rules/no-unknown-returns.ts
8
+ /**
9
+ * Whether the node sits in a type pattern rather than a contract: the `extends`
10
+ * clause of a conditional type or a type parameter constraint. There
11
+ * `(...args: never[]) => unknown` matches any function; it promises nothing.
12
+ * (Local change: upstream reports these.)
13
+ */
14
+ function isInsideTypePattern(node) {
15
+ let child = node;
16
+ let parent = node.parent;
17
+ while (parent !== null && parent.type !== "Program") {
18
+ if (parent.type === "TSConditionalType" && parent.extendsType === child) return true;
19
+ if (parent.type === "TSTypeParameter" && parent.constraint === child) return true;
20
+ child = parent;
21
+ parent = parent.parent;
22
+ }
23
+ return false;
24
+ }
25
+ /** Ban function contracts that return unknown instead of a parsed domain type. */
26
+ const noUnknownReturns = define({
27
+ name: "no-unknown-returns",
28
+ meta: meta({
29
+ type: "problem",
30
+ description: "Disallow functions whose explicit return contract is unknown or Promise<unknown>.",
31
+ messages: { unknownReturn: "This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type." }
32
+ }),
33
+ create: function* () {
34
+ const context = yield* RuleContext;
35
+ let environment = null;
36
+ const resolvesToUnknown = (type) => environment !== null && resolvedTypeMatches(type, environment, (resolved, matches) => {
37
+ if (resolved.type === "TSUnknownKeyword") return true;
38
+ if (resolved.type === "TSParenthesizedType") return matches(resolved.typeAnnotation);
39
+ if (resolved.type === "TSUnionType") return resolved.types.some(matches);
40
+ if (resolved.type !== "TSTypeReference" || resolved.typeName.type !== "Identifier" || resolved.typeName.name !== "Promise" && resolved.typeName.name !== "PromiseLike") return false;
41
+ const value = resolved.typeArguments?.params[0];
42
+ return value !== void 0 && matches(value);
43
+ });
44
+ const checkReturnType = (node) => {
45
+ const annotation = node.returnType;
46
+ if (annotation === null || annotation === void 0) return Effect.void;
47
+ if (!resolvesToUnknown(annotation.typeAnnotation)) return Effect.void;
48
+ if (isInsideTypePattern(node)) return Effect.void;
49
+ return context.report(fromId({
50
+ node: annotation.typeAnnotation,
51
+ messageId: "unknownReturn"
52
+ }));
53
+ };
54
+ return {
55
+ Program: (node) => {
56
+ environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
57
+ return Effect.void;
58
+ },
59
+ ArrowFunctionExpression: checkReturnType,
60
+ FunctionDeclaration: checkReturnType,
61
+ FunctionExpression: checkReturnType,
62
+ TSCallSignatureDeclaration: checkReturnType,
63
+ TSConstructSignatureDeclaration: checkReturnType,
64
+ TSConstructorType: checkReturnType,
65
+ TSDeclareFunction: checkReturnType,
66
+ TSEmptyBodyFunctionExpression: checkReturnType,
67
+ TSFunctionType: checkReturnType,
68
+ TSMethodSignature: checkReturnType
69
+ };
70
+ }
71
+ });
72
+ //#endregion
73
+ export { noUnknownReturns };