@timmo001/oxlint-rules 0.2.1 → 0.3.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.
package/dist/cli.js CHANGED
@@ -1,168 +1,211 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/effect/index.ts
3
+ // src/cli.ts
4
+ import { resolve as resolve2 } from "node:path";
5
+
6
+ // node_modules/oxlint/dist/index.js
7
+ function defineConfig(config) {
8
+ return config;
9
+ }
10
+
11
+ // vendor/anti-slop/src/index.ts
4
12
  import { eslintCompatPlugin } from "@oxlint/plugins";
5
13
 
6
- // src/effect/rules/no-try-catch-in-effect-generators.ts
14
+ // vendor/anti-slop/src/rules/no-array-filter-map.ts
7
15
  import { defineRule } from "@oxlint/plugins";
8
- function resolveVariable(sourceCode, identifier) {
9
- let scope = sourceCode.getScope(identifier);
10
- while (scope) {
11
- const variable = scope.set.get(identifier.name);
12
- if (variable)
16
+
17
+ // vendor/anti-slop/src/shared/array-method.ts
18
+ function unwrapArrayExpression(node) {
19
+ while (node.type === "ParenthesizedExpression" || node.type === "ChainExpression" || node.type === "TSAsExpression" || node.type === "TSTypeAssertion" || node.type === "TSNonNullExpression" || node.type === "TSSatisfiesExpression") {
20
+ node = node.expression;
21
+ }
22
+ return node;
23
+ }
24
+ function resolveArrayBinding(sourceCode, node) {
25
+ node = unwrapArrayExpression(node);
26
+ if (node.type !== "Identifier")
27
+ return null;
28
+ let scope = sourceCode.getScope(node);
29
+ while (scope !== null) {
30
+ const variable = scope.set.get(node.name);
31
+ if (variable !== undefined)
13
32
  return variable;
14
33
  scope = scope.upper;
15
34
  }
16
35
  return null;
17
36
  }
18
- function nearestEnclosingFunction(node) {
19
- let current = node.parent;
20
- while (current) {
21
- if (current.type === "FunctionDeclaration" || current.type === "FunctionExpression" || current.type === "ArrowFunctionExpression") {
22
- return current;
23
- }
24
- current = current.parent;
37
+ function arrayMethodTarget(node) {
38
+ node = unwrapArrayExpression(node);
39
+ if (node.type !== "MemberExpression")
40
+ return null;
41
+ const property = node.property;
42
+ if (!node.computed && property.type === "Identifier") {
43
+ return { name: property.name, object: node.object };
44
+ }
45
+ if (node.computed && property.type === "Literal" && typeof property.value === "string") {
46
+ return { name: property.value, object: node.object };
25
47
  }
26
48
  return null;
27
49
  }
28
- function staticMemberName(node) {
29
- if (node.type !== "MemberExpression" || node.computed)
30
- return null;
31
- return node.property.type === "Identifier" ? node.property.name : null;
32
- }
33
- function isEffectImport(sourceCode, identifier, namespace) {
34
- const variable = resolveVariable(sourceCode, identifier);
35
- return variable?.defs.some((definition) => {
36
- if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration" || definition.parent.source.value !== "effect") {
37
- return false;
38
- }
39
- if (namespace)
40
- return definition.node.type === "ImportNamespaceSpecifier";
41
- if (definition.node.type !== "ImportSpecifier")
42
- return false;
43
- const imported = definition.node.imported;
44
- return (imported.type === "Identifier" ? imported.name : imported.value) === "Effect";
45
- }) ?? false;
46
- }
47
- function isEffectMethod(sourceCode, node, method) {
48
- if (staticMemberName(node) !== method || node.type !== "MemberExpression") {
49
- return false;
50
+ function isArrayAnnotation(type) {
51
+ if (type.type === "TSArrayType" || type.type === "TSTupleType")
52
+ return true;
53
+ if (type.type === "TSParenthesizedType")
54
+ return isArrayAnnotation(type.typeAnnotation);
55
+ if (type.type === "TSTypeOperator" && type.operator === "readonly") {
56
+ return isArrayAnnotation(type.typeAnnotation);
50
57
  }
51
- const object = node.object;
52
- if (object.type === "Identifier") {
53
- return isEffectImport(sourceCode, object, false);
58
+ return type.type === "TSTypeReference" && type.typeName.type === "Identifier" && (type.typeName.name === "Array" || type.typeName.name === "ReadonlyArray");
59
+ }
60
+ function isKnownArrayExpression(sourceCode, node, visited = new Set) {
61
+ node = unwrapArrayExpression(node);
62
+ if (node.type === "ArrayExpression")
63
+ return true;
64
+ if (node.type === "CallExpression") {
65
+ const method = arrayMethodTarget(node.callee);
66
+ return method !== null && ["map", "filter", "flatMap", "slice", "concat", "toSorted", "toReversed", "toSpliced"].includes(method.name) && isKnownArrayExpression(sourceCode, method.object, visited);
54
67
  }
55
- if (staticMemberName(object) !== "Effect" || object.type !== "MemberExpression" || object.object.type !== "Identifier") {
68
+ if (node.type !== "Identifier")
56
69
  return false;
57
- }
58
- return isEffectImport(sourceCode, object.object, true);
59
- }
60
- function isDirectArgument(owner, call) {
61
- return call.arguments.some((argument) => argument === owner);
62
- }
63
- function isRecognisedEffectGenerator(sourceCode, owner) {
64
- const parent = owner.parent;
65
- if (parent.type !== "CallExpression" || !isDirectArgument(owner, parent)) {
70
+ const variable = resolveArrayBinding(sourceCode, node);
71
+ if (variable === null || visited.has(variable))
66
72
  return false;
73
+ visited.add(variable);
74
+ if (variable.references.some((reference) => reference.isWrite() && !reference.init))
75
+ return false;
76
+ for (const identifier of variable.identifiers) {
77
+ const annotation = identifier.typeAnnotation?.typeAnnotation;
78
+ if (annotation !== undefined)
79
+ return isArrayAnnotation(annotation);
67
80
  }
68
- if (isEffectMethod(sourceCode, parent.callee, "gen"))
69
- return true;
70
- const factoryCall = parent.callee;
71
- return factoryCall.type === "CallExpression" && isEffectMethod(sourceCode, factoryCall.callee, "fn");
81
+ for (const definition of variable.defs) {
82
+ 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") {
83
+ return isKnownArrayExpression(sourceCode, definition.node.init, visited);
84
+ }
85
+ }
86
+ return false;
72
87
  }
73
- var noTryCatchInEffectGeneratorsRule = defineRule({
88
+
89
+ // vendor/anti-slop/src/rules/no-array-filter-map.ts
90
+ var noArrayFilterMapRule = defineRule({
74
91
  meta: {
75
- type: "problem",
76
- docs: {
77
- description: "Disallow synchronous try/catch owned by recognised Effect generator callbacks."
78
- },
92
+ type: "suggestion",
93
+ docs: { description: "Disallow adjacent array filter/map passes in favor of lazy iterator helpers or a single transformation." },
79
94
  messages: {
80
- useEffectErrorChannel: "Keep expected failures in the Effect error channel. Use Effect.try for synchronous throwing work, Effect.tryPromise for asynchronous throwing work, Effect-returning schema APIs for decoding, and Effect recovery combinators for recovery."
95
+ arrayFilterMap: "Avoid consecutive array `{{first}}` and `{{second}}` passes. Prefer `.values().{{first}}(...).{{second}}(...).toArray()` where iterator helpers are supported, or a single `flatMap`/mutating reducer. Preserve callback ordering, indexes, and filtering semantics."
81
96
  }
82
97
  },
83
- create(context) {
98
+ createOnce(context) {
84
99
  return {
85
- TryStatement(node) {
86
- if (!node.handler)
100
+ CallExpression(node) {
101
+ const outer = arrayMethodTarget(node.callee);
102
+ if (outer === null || outer.name !== "map" && outer.name !== "filter")
87
103
  return;
88
- const owner = nearestEnclosingFunction(node);
89
- if (!owner?.generator || !isRecognisedEffectGenerator(context.sourceCode, owner)) {
104
+ const innerCall = unwrapArrayExpression(outer.object);
105
+ if (innerCall.type !== "CallExpression")
90
106
  return;
91
- }
92
- context.report({ node, messageId: "useEffectErrorChannel" });
107
+ const inner = arrayMethodTarget(innerCall.callee);
108
+ if (inner === null || inner.name !== (outer.name === "map" ? "filter" : "map"))
109
+ return;
110
+ if (!isKnownArrayExpression(context.sourceCode, inner.object))
111
+ return;
112
+ context.report({ node, messageId: "arrayFilterMap", data: { first: inner.name, second: outer.name } });
93
113
  }
94
114
  };
95
115
  }
96
116
  });
97
117
 
98
- // src/effect/index.ts
99
- var timmoEffectPlugin = eslintCompatPlugin({
100
- meta: { name: "timmo-effect" },
101
- rules: {
102
- "no-try-catch-in-effect-generators": noTryCatchInEffectGeneratorsRule
103
- }
104
- });
105
- var effect_default = timmoEffectPlugin;
106
-
107
- // vendor/anti-slop/src/effect/index.ts
108
- import { eslintCompatPlugin as eslintCompatPlugin2 } from "@oxlint/plugins";
109
-
110
- // vendor/anti-slop/src/effect/rules/no-service-constructor-imports.ts
118
+ // vendor/anti-slop/src/rules/no-reduce-accumulator-copy.ts
111
119
  import { defineRule as defineRule2 } from "@oxlint/plugins";
112
- var SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u;
113
- var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
114
- function isProjectLocalImport(source) {
115
- return source.startsWith("./") || source.startsWith("../");
120
+ function enclosingReducer(node) {
121
+ let parent = node.parent;
122
+ while (parent !== null) {
123
+ if (parent.type === "FunctionDeclaration")
124
+ return null;
125
+ if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") {
126
+ const callback = parent;
127
+ let owner = callback.parent;
128
+ while (owner !== null && unwrapArrayExpression(owner) === callback)
129
+ owner = owner.parent;
130
+ if (owner?.type !== "CallExpression")
131
+ return null;
132
+ const method = arrayMethodTarget(owner.callee);
133
+ const firstArgument = owner.arguments[0];
134
+ if (method === null || method.name !== "reduce" && method.name !== "reduceRight" || owner.arguments.length > 2 || firstArgument === undefined || unwrapArrayExpression(firstArgument) !== callback)
135
+ return null;
136
+ const firstParameter = callback.params[0];
137
+ const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter;
138
+ if (accumulator?.type !== "Identifier")
139
+ return null;
140
+ return { callback, accumulator, initialValue: owner.arguments[1] };
141
+ }
142
+ parent = parent.parent;
143
+ }
144
+ return null;
116
145
  }
117
- function getImportedName(specifier) {
118
- if (specifier.imported.type === "Identifier")
119
- return specifier.imported.name;
120
- return specifier.imported.value;
146
+ function referencesAccumulator(sourceCode, node, accumulator, visited = new Set) {
147
+ const variable = resolveArrayBinding(sourceCode, node);
148
+ if (variable === null || visited.has(variable))
149
+ return false;
150
+ if (variable === accumulator)
151
+ return true;
152
+ visited.add(variable);
153
+ if (variable.references.some((reference) => reference.isWrite() && !reference.init))
154
+ return false;
155
+ for (const definition of variable.defs) {
156
+ 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") {
157
+ return referencesAccumulator(sourceCode, definition.node.init, accumulator, visited);
158
+ }
159
+ }
160
+ return false;
161
+ }
162
+ function isGlobalCopyOwner(sourceCode, node, name) {
163
+ node = unwrapArrayExpression(node);
164
+ if (node.type !== "Identifier" || node.name !== name)
165
+ return false;
166
+ const variable = resolveArrayBinding(sourceCode, node);
167
+ return variable === null || variable.defs.length === 0;
121
168
  }
122
- var noServiceConstructorImportsRule = defineRule2({
169
+ var noReduceAccumulatorCopyRule = defineRule2({
123
170
  meta: {
124
171
  type: "problem",
125
- docs: {
126
- description: "Disallow project-local make<CapabilityName> imports outside test and spec files."
127
- },
172
+ docs: { description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods." },
128
173
  messages: {
129
- serviceConstructorImport: 'Do not import Effect service constructor "{{name}}" into runtime code. Import the owning Layer, yield the contextual service, and allow its requirements to propagate to the composition root.'
174
+ 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."
130
175
  }
131
176
  },
132
- create(context) {
133
- const isTestFile = TEST_FILE.test(context.filename.replaceAll("\\", "/"));
177
+ createOnce(context) {
134
178
  return {
135
- ImportDeclaration(node) {
136
- if (isTestFile || !isProjectLocalImport(node.source.value))
179
+ CallExpression(node) {
180
+ const method = arrayMethodTarget(node.callee);
181
+ if (method === null)
137
182
  return;
138
- for (const specifier of node.specifiers) {
139
- if (specifier.type !== "ImportSpecifier")
140
- continue;
141
- const importedName = getImportedName(specifier);
142
- if (!SERVICE_CONSTRUCTOR_NAME.test(importedName))
143
- continue;
144
- context.report({
145
- node: specifier,
146
- messageId: "serviceConstructorImport",
147
- data: { name: importedName }
148
- });
183
+ const reducer = enclosingReducer(node);
184
+ if (reducer === null)
185
+ return;
186
+ const accumulator = context.sourceCode.getDeclaredVariables(reducer.callback).find((variable) => variable.identifiers.some((identifier) => identifier.start === reducer.accumulator.start));
187
+ if (accumulator === undefined)
188
+ return;
189
+ const isAccumulator = (expression) => referencesAccumulator(context.sourceCode, expression, accumulator);
190
+ let copiesAccumulator = false;
191
+ if (method.name === "assign" && isGlobalCopyOwner(context.sourceCode, method.object, "Object")) {
192
+ const target = node.arguments[0];
193
+ copiesAccumulator = target !== undefined && unwrapArrayExpression(target).type === "ObjectExpression" && node.arguments.slice(1).some(isAccumulator);
194
+ } else if (method.name === "from" && isGlobalCopyOwner(context.sourceCode, method.object, "Array")) {
195
+ const source = node.arguments[0];
196
+ copiesAccumulator = source !== undefined && isAccumulator(source);
197
+ } else if (["concat", "slice", "toSpliced", "toSorted", "toReversed", "with"].includes(method.name)) {
198
+ const initialValue = reducer.initialValue;
199
+ const arrayAccumulator = initialValue !== undefined && isKnownArrayExpression(context.sourceCode, initialValue);
200
+ copiesAccumulator = arrayAccumulator && isAccumulator(method.object);
149
201
  }
202
+ if (copiesAccumulator)
203
+ context.report({ node, messageId: "accumulatorCopy" });
150
204
  }
151
205
  };
152
206
  }
153
207
  });
154
208
 
155
- // vendor/anti-slop/src/effect/index.ts
156
- var antiSlopEffectPlugin = eslintCompatPlugin2({
157
- meta: { name: "anti-slop-effect" },
158
- rules: {
159
- "no-service-constructor-imports": noServiceConstructorImportsRule
160
- }
161
- });
162
- var effect_default2 = antiSlopEffectPlugin;
163
- // vendor/anti-slop/src/index.ts
164
- import { eslintCompatPlugin as eslintCompatPlugin3 } from "@oxlint/plugins";
165
-
166
209
  // vendor/anti-slop/src/rules/no-chained-type-assertions.ts
167
210
  import { defineRule as defineRule3 } from "@oxlint/plugins";
168
211
  function isTypeAssertionExpression(node) {
@@ -665,9 +708,9 @@ function classifyWideningTarget(type, environment) {
665
708
  if (alias === null)
666
709
  return null;
667
710
  if ((alias.typeParameters?.params.length ?? 0) > 0) {
668
- const substitutions2 = aliasSubstitution(alias, unwrapped, new Map);
669
- const resolved2 = substitutions2 === null ? null : classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions2, new Set([name]));
670
- return resolved2?.kind === "open dictionary" ? { kind: "generic container" } : null;
711
+ const substitutions = aliasSubstitution(alias, unwrapped, new Map);
712
+ const resolved = substitutions === null ? null : classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions, new Set([name]));
713
+ return resolved?.kind === "open dictionary" ? { kind: "generic container" } : null;
671
714
  }
672
715
  const substitutions = aliasSubstitution(alias, unwrapped, new Map);
673
716
  if (substitutions === null)
@@ -799,7 +842,7 @@ function unwrapExpression(expression) {
799
842
  }
800
843
  return current;
801
844
  }
802
- function resolveVariable2(sourceCode, identifier) {
845
+ function resolveVariable(sourceCode, identifier) {
803
846
  let scope = sourceCode.getScope(identifier);
804
847
  while (scope !== null) {
805
848
  const variable = scope.set.get(identifier.name);
@@ -824,7 +867,7 @@ function hasKnownEvidence(sourceCode, expression, visitedVariables = new Set) {
824
867
  const unwrapped = unwrapExpression(expression);
825
868
  if (unwrapped.type !== "Identifier")
826
869
  return false;
827
- const variable = resolveVariable2(sourceCode, unwrapped);
870
+ const variable = resolveVariable(sourceCode, unwrapped);
828
871
  if (variable === null || visitedVariables.has(variable))
829
872
  return false;
830
873
  const declarator = variableDeclarator(variable);
@@ -843,7 +886,7 @@ function localFunctionForCall(sourceCode, callee) {
843
886
  return unwrapped;
844
887
  if (unwrapped.type !== "Identifier")
845
888
  return null;
846
- const variable = resolveVariable2(sourceCode, unwrapped);
889
+ const variable = resolveVariable(sourceCode, unwrapped);
847
890
  if (variable === null || variable.defs.length !== 1)
848
891
  return null;
849
892
  const [definition] = variable.defs;
@@ -896,7 +939,7 @@ function hasKnownCallArgumentEvidence(sourceCode, expression, environment, visit
896
939
  }
897
940
  if (expression.type !== "Identifier")
898
941
  return isKnownEvidenceExpression(expression);
899
- const variable = resolveVariable2(sourceCode, expression);
942
+ const variable = resolveVariable(sourceCode, expression);
900
943
  if (variable === null || visitedVariables.has(variable))
901
944
  return false;
902
945
  const annotation = variableTypeAnnotation(sourceCode, variable);
@@ -1010,7 +1053,7 @@ var noKnownValueWideningRule = defineRule5({
1010
1053
  AssignmentExpression(node) {
1011
1054
  if (node.operator !== "=" || node.left.type !== "Identifier")
1012
1055
  return;
1013
- const variable = resolveVariable2(context.sourceCode, node.left);
1056
+ const variable = resolveVariable(context.sourceCode, node.left);
1014
1057
  if (variable === null)
1015
1058
  return;
1016
1059
  const declarator = variableDeclarator(variable);
@@ -1076,7 +1119,7 @@ var noKnownValueWideningRule = defineRule5({
1076
1119
  // vendor/anti-slop/src/rules/no-module-mocking.ts
1077
1120
  import { defineRule as defineRule6 } from "@oxlint/plugins";
1078
1121
  var moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]);
1079
- function resolveVariable3(sourceCode, identifier) {
1122
+ function resolveVariable2(sourceCode, identifier) {
1080
1123
  let scope = sourceCode.getScope(identifier);
1081
1124
  while (scope !== null) {
1082
1125
  const variable = scope.set.get(identifier.name);
@@ -1097,7 +1140,7 @@ function isTestFrameworkObject(sourceCode, expression) {
1097
1140
  if ((expression.name === "vi" || expression.name === "jest") && sourceCode.isGlobalReference(expression)) {
1098
1141
  return true;
1099
1142
  }
1100
- const variable = resolveVariable3(sourceCode, expression);
1143
+ const variable = resolveVariable2(sourceCode, expression);
1101
1144
  if (variable === null || variable.defs.length === 0) {
1102
1145
  return expression.name === "vi" || expression.name === "jest";
1103
1146
  }
@@ -1200,7 +1243,7 @@ var noObjectParametersRule = defineRule7({
1200
1243
  import { defineRule as defineRule8 } from "@oxlint/plugins";
1201
1244
 
1202
1245
  // vendor/anti-slop/src/shared/reflect-method.ts
1203
- function resolveVariable4(sourceCode, identifier) {
1246
+ function resolveVariable3(sourceCode, identifier) {
1204
1247
  let scope = sourceCode.getScope(identifier);
1205
1248
  while (scope !== null) {
1206
1249
  const variable = scope.set.get(identifier.name);
@@ -1215,7 +1258,7 @@ function isGlobalReflect(sourceCode, expression) {
1215
1258
  return false;
1216
1259
  if (sourceCode.isGlobalReference(expression))
1217
1260
  return true;
1218
- const variable = resolveVariable4(sourceCode, expression);
1261
+ const variable = resolveVariable3(sourceCode, expression);
1219
1262
  return variable === null || variable.defs.length === 0;
1220
1263
  }
1221
1264
  function isGlobalReflectMethodCall(sourceCode, callee, methodName) {
@@ -1974,9 +2017,11 @@ var requireSafetyCommentForTypeAssertionRule = defineRule17({
1974
2017
  });
1975
2018
 
1976
2019
  // vendor/anti-slop/src/index.ts
1977
- var antiSlopPlugin = eslintCompatPlugin3({
2020
+ var antiSlopPlugin = eslintCompatPlugin({
1978
2021
  meta: { name: "anti-slop" },
1979
2022
  rules: {
2023
+ "no-array-filter-map": noArrayFilterMapRule,
2024
+ "no-reduce-accumulator-copy": noReduceAccumulatorCopyRule,
1980
2025
  "no-chained-type-assertions": noChainedTypeAssertionsRule,
1981
2026
  "no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
1982
2027
  "no-known-value-widening": noKnownValueWideningRule,
@@ -1996,11 +2041,11 @@ var antiSlopPlugin = eslintCompatPlugin3({
1996
2041
  });
1997
2042
  var src_default = antiSlopPlugin;
1998
2043
  // src/generic/index.ts
1999
- import { eslintCompatPlugin as eslintCompatPlugin4 } from "@oxlint/plugins";
2044
+ import { eslintCompatPlugin as eslintCompatPlugin2 } from "@oxlint/plugins";
2000
2045
 
2001
2046
  // src/generic/rules/prefer-event-parameter-type.ts
2002
2047
  import { defineRule as defineRule18 } from "@oxlint/plugins";
2003
- function nearestEnclosingFunction2(node) {
2048
+ function nearestEnclosingFunction(node) {
2004
2049
  let current = node.parent;
2005
2050
  while (current) {
2006
2051
  if (current.type === "FunctionDeclaration" || current.type === "FunctionExpression" || current.type === "ArrowFunctionExpression") {
@@ -2017,7 +2062,7 @@ function assertedEventParameter(node) {
2017
2062
  }
2018
2063
  const parameterName = expression.object.name;
2019
2064
  const property = expression.property.name;
2020
- const owner = nearestEnclosingFunction2(node);
2065
+ const owner = nearestEnclosingFunction(node);
2021
2066
  if (!owner?.params.some((parameter) => parameter.type === "Identifier" && parameter.name === parameterName)) {
2022
2067
  return null;
2023
2068
  }
@@ -2055,7 +2100,7 @@ var preferEventParameterTypeRule = defineRule18({
2055
2100
  });
2056
2101
 
2057
2102
  // src/generic/index.ts
2058
- var timmoPlugin = eslintCompatPlugin4({
2103
+ var timmoPlugin = eslintCompatPlugin2({
2059
2104
  meta: { name: "timmo" },
2060
2105
  rules: {
2061
2106
  "prefer-event-parameter-type": preferEventParameterTypeRule
@@ -2063,11 +2108,6 @@ var timmoPlugin = eslintCompatPlugin4({
2063
2108
  });
2064
2109
  var generic_default = timmoPlugin;
2065
2110
 
2066
- // node_modules/oxlint/dist/index.js
2067
- function defineConfig(config) {
2068
- return config;
2069
- }
2070
-
2071
2111
  // src/configs/enable-plugin-rules.ts
2072
2112
  function enablePluginRules(namespace, plugin) {
2073
2113
  return Object.fromEntries(Object.keys(plugin.rules).sort().map((rule) => [`${namespace}/${rule}`, "error"]));
@@ -2089,8 +2129,168 @@ var recommended = defineConfig({
2089
2129
  });
2090
2130
  var recommended_default = recommended;
2091
2131
 
2092
- // src/configs/effect.ts
2093
- var effect = defineConfig({
2132
+ // src/effect/index.ts
2133
+ import { eslintCompatPlugin as eslintCompatPlugin3 } from "@oxlint/plugins";
2134
+
2135
+ // src/effect/rules/no-try-catch-in-effect-generators.ts
2136
+ import { defineRule as defineRule19 } from "@oxlint/plugins";
2137
+ function resolveVariable4(sourceCode, identifier) {
2138
+ let scope = sourceCode.getScope(identifier);
2139
+ while (scope) {
2140
+ const variable = scope.set.get(identifier.name);
2141
+ if (variable)
2142
+ return variable;
2143
+ scope = scope.upper;
2144
+ }
2145
+ return null;
2146
+ }
2147
+ function nearestEnclosingFunction2(node) {
2148
+ let current = node.parent;
2149
+ while (current) {
2150
+ if (current.type === "FunctionDeclaration" || current.type === "FunctionExpression" || current.type === "ArrowFunctionExpression") {
2151
+ return current;
2152
+ }
2153
+ current = current.parent;
2154
+ }
2155
+ return null;
2156
+ }
2157
+ function staticMemberName(node) {
2158
+ if (node.type !== "MemberExpression" || node.computed)
2159
+ return null;
2160
+ return node.property.type === "Identifier" ? node.property.name : null;
2161
+ }
2162
+ function isEffectImport(sourceCode, identifier, namespace) {
2163
+ const variable = resolveVariable4(sourceCode, identifier);
2164
+ return variable?.defs.some((definition) => {
2165
+ if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration" || definition.parent.source.value !== "effect") {
2166
+ return false;
2167
+ }
2168
+ if (namespace)
2169
+ return definition.node.type === "ImportNamespaceSpecifier";
2170
+ if (definition.node.type !== "ImportSpecifier")
2171
+ return false;
2172
+ const imported = definition.node.imported;
2173
+ return (imported.type === "Identifier" ? imported.name : imported.value) === "Effect";
2174
+ }) ?? false;
2175
+ }
2176
+ function isEffectMethod(sourceCode, node, method) {
2177
+ if (staticMemberName(node) !== method || node.type !== "MemberExpression") {
2178
+ return false;
2179
+ }
2180
+ const object = node.object;
2181
+ if (object.type === "Identifier") {
2182
+ return isEffectImport(sourceCode, object, false);
2183
+ }
2184
+ if (staticMemberName(object) !== "Effect" || object.type !== "MemberExpression" || object.object.type !== "Identifier") {
2185
+ return false;
2186
+ }
2187
+ return isEffectImport(sourceCode, object.object, true);
2188
+ }
2189
+ function isDirectArgument(owner, call) {
2190
+ return call.arguments.some((argument) => argument === owner);
2191
+ }
2192
+ function isRecognisedEffectGenerator(sourceCode, owner) {
2193
+ const parent = owner.parent;
2194
+ if (parent.type !== "CallExpression" || !isDirectArgument(owner, parent)) {
2195
+ return false;
2196
+ }
2197
+ if (isEffectMethod(sourceCode, parent.callee, "gen"))
2198
+ return true;
2199
+ const factoryCall = parent.callee;
2200
+ return factoryCall.type === "CallExpression" && isEffectMethod(sourceCode, factoryCall.callee, "fn");
2201
+ }
2202
+ var noTryCatchInEffectGeneratorsRule = defineRule19({
2203
+ meta: {
2204
+ type: "problem",
2205
+ docs: {
2206
+ description: "Disallow synchronous try/catch owned by recognised Effect generator callbacks."
2207
+ },
2208
+ messages: {
2209
+ useEffectErrorChannel: "Keep expected failures in the Effect error channel. Use Effect.try for synchronous throwing work, Effect.tryPromise for asynchronous throwing work, Effect-returning schema APIs for decoding, and Effect recovery combinators for recovery."
2210
+ }
2211
+ },
2212
+ create(context) {
2213
+ return {
2214
+ TryStatement(node) {
2215
+ if (!node.handler)
2216
+ return;
2217
+ const owner = nearestEnclosingFunction2(node);
2218
+ if (!owner?.generator || !isRecognisedEffectGenerator(context.sourceCode, owner)) {
2219
+ return;
2220
+ }
2221
+ context.report({ node, messageId: "useEffectErrorChannel" });
2222
+ }
2223
+ };
2224
+ }
2225
+ });
2226
+
2227
+ // src/effect/index.ts
2228
+ var timmoEffectPlugin = eslintCompatPlugin3({
2229
+ meta: { name: "timmo-effect" },
2230
+ rules: {
2231
+ "no-try-catch-in-effect-generators": noTryCatchInEffectGeneratorsRule
2232
+ }
2233
+ });
2234
+ var effect_default = timmoEffectPlugin;
2235
+
2236
+ // vendor/anti-slop/src/effect/index.ts
2237
+ import { eslintCompatPlugin as eslintCompatPlugin4 } from "@oxlint/plugins";
2238
+
2239
+ // vendor/anti-slop/src/effect/rules/no-service-constructor-imports.ts
2240
+ import { defineRule as defineRule20 } from "@oxlint/plugins";
2241
+ var SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u;
2242
+ var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
2243
+ function isProjectLocalImport(source) {
2244
+ return source.startsWith("./") || source.startsWith("../");
2245
+ }
2246
+ function getImportedName(specifier) {
2247
+ if (specifier.imported.type === "Identifier")
2248
+ return specifier.imported.name;
2249
+ return specifier.imported.value;
2250
+ }
2251
+ var noServiceConstructorImportsRule = defineRule20({
2252
+ meta: {
2253
+ type: "problem",
2254
+ docs: {
2255
+ description: "Disallow project-local make<CapabilityName> imports outside test and spec files."
2256
+ },
2257
+ messages: {
2258
+ serviceConstructorImport: 'Do not import Effect service constructor "{{name}}" into runtime code. Import the owning Layer, yield the contextual service, and allow its requirements to propagate to the composition root.'
2259
+ }
2260
+ },
2261
+ create(context) {
2262
+ const isTestFile = TEST_FILE.test(context.filename.replaceAll("\\", "/"));
2263
+ return {
2264
+ ImportDeclaration(node) {
2265
+ if (isTestFile || !isProjectLocalImport(node.source.value))
2266
+ return;
2267
+ for (const specifier of node.specifiers) {
2268
+ if (specifier.type !== "ImportSpecifier")
2269
+ continue;
2270
+ const importedName = getImportedName(specifier);
2271
+ if (!SERVICE_CONSTRUCTOR_NAME.test(importedName))
2272
+ continue;
2273
+ context.report({
2274
+ node: specifier,
2275
+ messageId: "serviceConstructorImport",
2276
+ data: { name: importedName }
2277
+ });
2278
+ }
2279
+ }
2280
+ };
2281
+ }
2282
+ });
2283
+
2284
+ // vendor/anti-slop/src/effect/index.ts
2285
+ var antiSlopEffectPlugin = eslintCompatPlugin4({
2286
+ meta: { name: "anti-slop-effect" },
2287
+ rules: {
2288
+ "no-service-constructor-imports": noServiceConstructorImportsRule
2289
+ }
2290
+ });
2291
+ var effect_default2 = antiSlopEffectPlugin;
2292
+ // src/configs/recommended-effect.ts
2293
+ var recommendedEffect = defineConfig({
2094
2294
  extends: [recommended_default],
2095
2295
  jsPlugins: [
2096
2296
  {
@@ -2104,10 +2304,7 @@ var effect = defineConfig({
2104
2304
  ...enablePluginRules("timmo-effect", effect_default)
2105
2305
  }
2106
2306
  });
2107
- var effect_default3 = effect;
2108
-
2109
- // src/cli.ts
2110
- import { resolve as resolve2 } from "node:path";
2307
+ var recommended_effect_default = recommendedEffect;
2111
2308
 
2112
2309
  // src/install/copy.ts
2113
2310
  import { cp, mkdir, readdir, rm } from "node:fs/promises";
@@ -2189,7 +2386,7 @@ if (command === "--help" || command === "-h") {
2189
2386
  console.log(` timmo: ${entries.timmo}`);
2190
2387
  console.log(` timmo-effect: ${entries.timmoEffect}`);
2191
2388
  printRuleSettings("General rule settings:", recommended_default.rules ?? {});
2192
- printRuleSettings("Effect rule settings (opt-in):", effect_default3.rules ?? {});
2389
+ printRuleSettings("Effect rule settings (opt-in):", recommended_effect_default.rules ?? {});
2193
2390
  } catch (error) {
2194
2391
  console.error(error instanceof Error ? error.message : String(error));
2195
2392
  process.exitCode = 1;