@commonplaces/oxlint-plugin 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,19 +1,23 @@
1
1
  # @commonplaces/oxlint-plugin
2
2
 
3
- 57 custom Oxlint rules for strict Effect TypeScript, Alchemy and general code
4
- checks. This package supports **Oxlint 1.80.0 or 1.85.0** and **Node 22.14 or newer**.
3
+ 61 custom Oxlint rules for strict Effect TypeScript, Alchemy and general code
4
+ checks. This package supports **Oxlint 1.80.0, 1.85.0 or 1.86.0** and **Node 22.14 or newer**.
5
5
  Oxlint's JavaScript plugin API is experimental, so upgrade it only after running
6
- the rule tests. The development tools use 1.80.0; consuming projects may use either supported version.
7
- Both exact versions are checked with independently installed archives. ESLint is not a supported host.
6
+ the rule tests. The development tools use 1.80.0; consuming projects may use any supported version.
7
+ All three exact versions are checked with independently installed archives. ESLint is not a supported host.
8
8
 
9
9
  ## Install
10
10
 
11
- Install from npm:
11
+ Install the checked GitHub release archive:
12
12
 
13
13
  ```sh
14
- npm install --save-dev --save-exact @commonplaces/oxlint-plugin@0.1.1 oxlint@1.80.0
14
+ npm install --save-dev --save-exact https://github.com/crcorbett/commonplace-lint/releases/download/v0.3.0/commonplaces-oxlint-plugin-0.3.0.tgz oxlint@1.80.0
15
15
  ```
16
16
 
17
+ The npm registry currently provides `@commonplaces/oxlint-plugin@0.1.2` (57 rules).
18
+ The 0.3.0 additions below require the GitHub archive until a separate npm release
19
+ is published. GitHub archive publication does not publish a registry version.
20
+
17
21
  Create `oxlint.config.ts`:
18
22
 
19
23
  ```ts
@@ -96,16 +100,72 @@ package subpaths; keep your existing rule settings.
96
100
 
97
101
  | Package subpath | Rule prefix | Rules |
98
102
  | ------------------- | ------------------- | ----: |
99
- | `/strict-effect` | `strict-effect/` | 27 |
103
+ | `/strict-effect` | `strict-effect/` | 29 |
100
104
  | `/effect` | `effect/` | 9 |
101
105
  | `/alchemy` | `alchemy-secrets/` | 3 |
102
106
  | `/bun` | `bun/` | 1 |
103
- | `/package` | `package/` | 1 |
107
+ | `/package` | `package/` | 3 |
104
108
  | `/anti-slop` | `anti-slop/` | 15 |
105
109
  | `/anti-slop-effect` | `anti-slop-effect/` | 1 |
106
110
 
107
111
  ## Development
108
112
 
113
+ ### Workspace import boundaries
114
+
115
+ Enable `package/workspace-import-boundaries` explicitly. Supply options derived
116
+ from checked, current workspace manifests at configuration ingress; the rule
117
+ does not read the filesystem or keep its own project inventory.
118
+
119
+ ```ts
120
+ "package/workspace-import-boundaries": ["error", {
121
+ owners: [
122
+ { id: "root", root: "." },
123
+ { id: "website", root: "apps/website" },
124
+ { id: "orders", root: "packages/orders" },
125
+ ],
126
+ workspaces: [{
127
+ name: "@example/orders",
128
+ owner: "orders",
129
+ exports: { ".": false, "./service": true, "./features/*": true, "./features/private/*": false },
130
+ }],
131
+ workspacePrefixes: ["@example/"],
132
+ forbiddenEdges: [{ from: "website", to: "orders" }],
133
+ exceptions: [{ fromFile: "apps/website/src/approved.ts", toModule: "@example/orders/service" }],
134
+ aliases: [{ prefix: "@website/", root: "apps/website/src" }],
135
+ }]
136
+ ```
137
+
138
+ The example is illustrative; derive the actual owners, package names and export
139
+ facts from manifests. Export booleans record reachability under the consumer's
140
+ selected conditions. Preserve explicit `null` fences as `false`. Exact exports
141
+ beat single-star patterns, and the most specific matching pattern wins. The
142
+ longest anchored root identifies an owner, including root composition files.
143
+ Cross-owner filesystem imports are rejected even when they reach a workspace
144
+ root or a non-`src` file. Exact exceptions can permit an owner edge, but cannot
145
+ expose a private export or permit a cross-owner filesystem path.
146
+
147
+ The check covers static imports, re-exports, type imports, import-equals,
148
+ unshadowed `require` and dynamic addresses resolvable through immutable local
149
+ strings. Unresolved computed addresses are outside its proof. Explicit aliases
150
+ are bounded directory prefixes; this is not a TypeScript, bundler, conditional
151
+ export or symlink resolver. Same-owner asset queries are allowed; `file:` imports
152
+ are unsupported and rejected. Invalid or contradictory options fail closed.
153
+
154
+ ### Subprocess pipe capture
155
+
156
+ The `effect` group enables `strict-effect/subprocess-pipe-capture`. It follows
157
+ native Effect process handles and rejects statically identified collection that
158
+ can wait for a child or one pipe while another captured pipe remains undrained.
159
+ Drain stdout, stderr and exit completion in one scope, for example with
160
+ `Effect.all({ stdout: read(handle.stdout), stderr: read(handle.stderr),
161
+ exitCode: handle.exitCode }, { concurrency: "unbounded" })`.
162
+
163
+ The rule accounts for known job order and concurrency, and accepts known
164
+ non-piped channels. It is bounded syntax analysis, not proof against every
165
+ process deadlock: computed jobs, unknown wrappers and unsupported control flow
166
+ still require review. A small-output test cannot establish safe capture. Test
167
+ both pipes beyond pipe capacity, a nonzero exit, UTF-8 and scoped cleanup.
168
+
109
169
  This workspace owns the rule code. Skills provide guidance and installation
110
170
  instructions; they do not carry another copy of the rules. The package contains
111
171
  compiled JavaScript and declarations, the README and the MIT licence. It does
@@ -129,3 +189,74 @@ or infrastructure stack inside the package.
129
189
 
130
190
  The strict Effect rules were adapted from Cooper's DAW rules at `e47ecfb` and
131
191
  subsequent Commonplace rules. The package's MIT licence covers this lint code.
192
+
193
+ ## Keep address ownership precise
194
+
195
+ `strict-effect/public-host-literals` can check test setup without replacing
196
+ independent expected values with the implementation under test:
197
+
198
+ ```ts
199
+ "strict-effect/public-host-literals": ["error", {
200
+ hosts: ["example.com"],
201
+ allowedFiles: ["packages/platform/src/site-address.ts"],
202
+ allowedDeclarations: [{
203
+ file: "packages/platform/src/site-address.ts",
204
+ names: ["siteHostName"],
205
+ }],
206
+ allowTestFiles: false,
207
+ allowTestAssertions: true,
208
+ }]
209
+ ```
210
+
211
+ The named top-level `const` declaration owns the literal hostname. Derive
212
+ `api` and `www` hostnames from it; use native deployment outputs for actual
213
+ stage-specific origins. Files with a declaration restriction do not gain a
214
+ whole-file exemption. Declaration entries cannot grant ownership to a file
215
+ absent from `allowedFiles`. Existing configurations without these optional
216
+ settings keep their previous behaviour.
217
+
218
+ The assertion exception permits recognised imported Vitest or `@effect/vitest`
219
+ assertions' direct expected arguments, including static arrays and objects.
220
+ It does not trace table-driven expected values through variables or callbacks;
221
+ keep those oracles in narrowly configured declarations or use a local
222
+ suppression explaining their role. It does
223
+ not exempt their actual arguments, setup values, constructors, arbitrary calls
224
+ or shadowed assertion names. A test using unrecognised assertion helpers can
225
+ keep an explicit, narrowly scoped lint suppression with its reason. The rule
226
+ checks written literal text; it cannot prove the value of arbitrary computed
227
+ strings or that a configured canonical owner is semantically correct.
228
+
229
+ ## Separate compiler assertions from runtime tests
230
+
231
+ The `effect` group enables `strict-effect/no-runtime-type-assertions`. It rejects
232
+ calls to imported Vitest or `@effect/vitest` `expectTypeOf` and `assertType` in
233
+ `.test.*` and `.spec.*` files. Aliases and namespace imports are recognised;
234
+ unrelated or shadowed local functions are not treated as Vitest APIs.
235
+
236
+ Move these checks into the existing TypeScript flow, for example
237
+ `inference.typecheck.ts`, or a configured Vitest `.test-d.ts` file. Include those
238
+ files in the compiler project and preserve negative brand/readonly assertions.
239
+ Do not create runtime services or registries only to feed a type assertion.
240
+ This syntax rule does not prove that the receiving compiler configuration
241
+ includes the destination file.
242
+
243
+ ## Register one owner for a prohibition
244
+
245
+ `package/no-redundant-rule-registrations` is opt-in. Configure pairs only after
246
+ checking that the owner covers the redundant rule under the receiving project's
247
+ options and file scopes:
248
+
249
+ ```ts
250
+ "package/no-redundant-rule-registrations": ["error", {
251
+ pairs: [{ owner: "effect/no-throw", redundant: "legacy/no-throw" }],
252
+ }]
253
+ ```
254
+
255
+ Those identifiers are illustrative consumer rules, not bundled equivalences.
256
+ The check reports when both members are statically enabled in the same Oxlint
257
+ `rules` object. Disabled owners do not establish redundancy. Separate overrides
258
+ are checked independently. An opaque spread or dynamic key clears preceding
259
+ known registrations; later explicit keys can establish them again. Imported or
260
+ spread contents still require effective-configuration review. Keep negative fixtures that
261
+ assert the intended diagnostic once and the redundant diagnostic is absent.
262
+ There is no automatic fix: removing a registration needs coverage evidence.
package/dist/alchemy.js CHANGED
@@ -2,97 +2,8 @@ import { defineRule, eslintCompatPlugin } from "@oxlint/plugins";
2
2
  import * as Array from "effect/Array";
3
3
  import * as Option from "effect/Option";
4
4
  import * as Predicate from "effect/Predicate";
5
- const variableInScope = (scope, name) => Array.findFirst((variable) => variable.name === name)(scope.variables).pipe(Option.orElse(() => Option.fromNullishOr(scope.upper).pipe(Option.flatMap((upper) => variableInScope(upper, name)))));
6
- const constant = (variable) => Array.head(variable.defs).pipe(Option.filter(() => variable.defs.length === 1), Option.filter(() => Array.every((reference) => !reference.isWrite() || reference.init)(variable.references)), Option.flatMap((definition) => definition.node.type === "VariableDeclarator" &&
7
- definition.parent?.type === "VariableDeclaration" &&
8
- definition.parent.kind === "const"
9
- ? Option.some(definition.node)
10
- : Option.none()));
11
- const propertyName = (node) => {
12
- if (node.type === "Identifier")
13
- return Option.some(node.name);
14
- if (node.type === "Literal" && Predicate.isString(node.value))
15
- return Option.some(node.value);
16
- return Option.none();
17
- };
18
- const member = (reference, name) => {
19
- if (Option.isSome(reference.name))
20
- return Option.none();
21
- return reference.module === "alchemy" ||
22
- reference.module === "effect" ||
23
- (reference.module === "effect/process" && name === "ChildProcess")
24
- ? Option.some({ module: `${reference.module}/${name}`, name: Option.none() })
25
- : Option.some({ ...reference, name: Option.some(name) });
26
- };
27
- // Only real imports and immutable local aliases count. The nearest declaration
28
- // prevents a shadowed parameter from borrowing an outer import's identity.
29
- const referenceOf = (context, node, visited = []) => {
30
- if (Array.some(visited, (previous) => previous === node))
31
- return Option.none();
32
- const next = [...visited, node];
33
- if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression") {
34
- return referenceOf(context, node.expression, next);
35
- }
36
- if (node.type === "MemberExpression") {
37
- const name = node.computed
38
- ? node.property.type === "Literal"
39
- ? propertyName(node.property)
40
- : Option.none()
41
- : propertyName(node.property);
42
- return Option.all([referenceOf(context, node.object, next), name]).pipe(Option.flatMap(([reference, key]) => member(reference, key)));
43
- }
44
- if (node.type !== "Identifier")
45
- return Option.none();
46
- return variableInScope(context.sourceCode.getScope(node), node.name).pipe(Option.flatMap((variable) => Array.head(variable.defs).pipe(Option.flatMap((definition) => {
47
- if (definition.type === "ImportBinding" &&
48
- definition.parent?.type === "ImportDeclaration" &&
49
- definition.parent.importKind !== "type") {
50
- const module = definition.parent.source.value;
51
- if (module !== "alchemy" &&
52
- !module.startsWith("alchemy/") &&
53
- module !== "effect" &&
54
- !module.startsWith("effect/"))
55
- return Option.none();
56
- const reference = { module, name: Option.none() };
57
- if (definition.node.type === "ImportNamespaceSpecifier")
58
- return Option.some(reference);
59
- if (definition.node.type === "ImportSpecifier" &&
60
- definition.node.importKind !== "type") {
61
- return propertyName(definition.node.imported).pipe(Option.flatMap((name) => member(reference, name)));
62
- }
63
- return Option.none();
64
- }
65
- return constant(variable).pipe(Option.flatMap((declaration) => Option.fromNullishOr(declaration.init).pipe(Option.flatMap((initialiser) => {
66
- if (declaration.id.type === "Identifier") {
67
- return referenceOf(context, initialiser, next);
68
- }
69
- if (declaration.id.type !== "ObjectPattern")
70
- return Option.none();
71
- return Array.findFirst((property) => property.type === "Property" &&
72
- property.value.type === "Identifier" &&
73
- property.value.name === node.name &&
74
- !property.computed)(declaration.id.properties).pipe(Option.flatMap((property) => property.type === "Property"
75
- ? Option.all([
76
- referenceOf(context, initialiser, next),
77
- propertyName(property.key),
78
- ]).pipe(Option.flatMap(([reference, key]) => member(reference, key)))
79
- : Option.none()));
80
- }))));
81
- }))));
82
- };
83
- const valueOf = (context, node, visited = []) => {
84
- if (Array.some(visited, (previous) => previous === node))
85
- return Option.none();
86
- const next = [...visited, node];
87
- if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression") {
88
- return valueOf(context, node.expression, next);
89
- }
90
- if (node.type !== "Identifier")
91
- return Option.some(node);
92
- return variableInScope(context.sourceCode.getScope(node), node.name).pipe(Option.flatMap(constant), Option.filter((declaration) => declaration.id.type === "Identifier"), Option.flatMap((declaration) => Option.fromNullishOr(declaration.init)), Option.flatMap((initialiser) => valueOf(context, initialiser, next)));
93
- };
94
- const operation = (context, node, module, name) => referenceOf(context, node).pipe(Option.exists((reference) => (reference.module === module && Option.contains(reference.name, name)) ||
95
- (reference.module === `${module}/${name}` && Option.isNone(reference.name))));
5
+ import { operation, valueOf } from "./native-reference.js";
6
+ import { propertyName } from "./syntax.js";
96
7
  const providerCall = (context, node, module, name) => valueOf(context, node).pipe(Option.filter((value) => value.type === "CallExpression"), Option.filter((value) => operation(context, value.callee, module, name)));
97
8
  const field = (node, name) => node.type === "ObjectExpression" &&
98
9
  Array.every((property) => property.type === "Property" && property.kind === "init" && !property.computed)(node.properties)
@@ -0,0 +1 @@
1
+ export declare const noRedundantRuleRegistrations: import("@oxlint/plugins").Rule;
package/dist/config.js ADDED
@@ -0,0 +1,204 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { Array, Option, Predicate, Record, Schema } from "effect";
3
+ import { childNodes, propertyName, variableInScope } from "./syntax.js";
4
+ const RuleId = Schema.String.check(Schema.isPattern(/^\S+$/u));
5
+ const Options = Schema.Struct({
6
+ pairs: Schema.NonEmptyArray(Schema.Struct({ owner: RuleId, redundant: RuleId })),
7
+ });
8
+ // Resolve only a single-use const. Another read could expose the object to a
9
+ // mutation or an opaque function before the configuration is consumed.
10
+ const constantValue = (variable, reference) => Array.head(variable.defs).pipe(Option.filter(() => variable.defs.length === 1), Option.filter(() => Array.every(variable.references, (entry) => (entry.init && entry.isWrite()) || entry.identifier === reference)), Option.flatMap((definition) => {
11
+ if (definition.node.type === "VariableDeclarator" &&
12
+ definition.node.id.type === "Identifier" &&
13
+ definition.parent?.type === "VariableDeclaration" &&
14
+ definition.parent.kind === "const") {
15
+ return Option.fromNullishOr(definition.node.init);
16
+ }
17
+ return Option.none();
18
+ }));
19
+ const staticValue = (context, node, visited = []) => {
20
+ if (Array.some(visited, (previous) => previous === node))
21
+ return Option.none();
22
+ const next = [...visited, node];
23
+ if (node.type === "TSAsExpression" ||
24
+ node.type === "TSSatisfiesExpression" ||
25
+ node.type === "TSNonNullExpression") {
26
+ return staticValue(context, node.expression, next);
27
+ }
28
+ if (node.type === "Identifier") {
29
+ return variableInScope(context.sourceCode.getScope(node), node.name).pipe(Option.flatMap((variable) => Array.some(variable.defs, (definition) => definition.type === "ImportBinding")
30
+ ? Option.some(node)
31
+ : constantValue(variable, node).pipe(Option.flatMap((value) => staticValue(context, value, next)))));
32
+ }
33
+ return Option.some(node);
34
+ };
35
+ // Spreads and dynamic keys invalidate earlier facts. Later explicit properties
36
+ // regain certainty; spread contents and other configuration scopes stay opaque.
37
+ const staticProperties = (context, node) => staticValue(context, node).pipe(Option.filter((value) => value.type === "ObjectExpression"), Option.flatMap((value) => {
38
+ if (value.type !== "ObjectExpression")
39
+ return Option.none();
40
+ const empty = {};
41
+ return Option.some(Array.reduce(value.properties, empty, (known, entry) => {
42
+ if (entry.type !== "Property")
43
+ return empty;
44
+ const key = entry.computed
45
+ ? entry.key.type === "Literal" && Predicate.isString(entry.key.value)
46
+ ? Option.some(entry.key.value)
47
+ : Option.none()
48
+ : propertyName(entry.key);
49
+ return Option.match(key, {
50
+ onNone: () => empty,
51
+ onSome: (name) => Record.set(known, name, entry),
52
+ });
53
+ }));
54
+ }));
55
+ const importedConfig = (context, node, namespace) => {
56
+ if (node.type !== "Identifier")
57
+ return false;
58
+ return variableInScope(context.sourceCode.getScope(node), node.name).pipe(Option.flatMap((variable) => Array.head(variable.defs)), Option.exists((definition) => {
59
+ if (definition.type !== "ImportBinding" ||
60
+ definition.parent?.type !== "ImportDeclaration" ||
61
+ definition.parent.source.value !== "oxlint" ||
62
+ definition.parent.importKind === "type") {
63
+ return false;
64
+ }
65
+ if (namespace)
66
+ return definition.node.type === "ImportNamespaceSpecifier";
67
+ return (definition.node.type === "ImportSpecifier" &&
68
+ definition.node.importKind !== "type" &&
69
+ Option.contains(propertyName(definition.node.imported), "defineConfig"));
70
+ }));
71
+ };
72
+ const configFunction = (context, node) => {
73
+ if (importedConfig(context, node, false))
74
+ return true;
75
+ return staticValue(context, node).pipe(Option.exists((value) => {
76
+ if (importedConfig(context, value, false))
77
+ return true;
78
+ if (value.type !== "MemberExpression")
79
+ return false;
80
+ return ((!value.computed || value.property.type === "Literal") &&
81
+ Option.contains(propertyName(value.property), "defineConfig") &&
82
+ importedConfig(context, value.object, true));
83
+ }));
84
+ };
85
+ const descendants = (context, node) => [
86
+ node,
87
+ ...Array.flatMap(childNodes(context, node), (child) => descendants(context, child)),
88
+ ];
89
+ const enabledSeverity = (node) => node.type === "Literal" &&
90
+ (node.value === "error" ||
91
+ node.value === "deny" ||
92
+ node.value === "warn" ||
93
+ node.value === 1 ||
94
+ node.value === 2);
95
+ const enabled = (context, node) => staticValue(context, node).pipe(Option.exists((value) => {
96
+ if (value.type === "Literal") {
97
+ return enabledSeverity(value);
98
+ }
99
+ if (value.type === "ArrayExpression") {
100
+ return Array.head(value.elements).pipe(Option.flatMap(Option.fromNullishOr), Option.flatMap((first) => staticValue(context, first)), Option.exists(enabledSeverity));
101
+ }
102
+ return false;
103
+ }));
104
+ export const noRedundantRuleRegistrations = defineRule({
105
+ meta: {
106
+ type: "problem",
107
+ docs: {
108
+ description: "Reject explicitly configured redundant lint registrations when their owner is also enabled.",
109
+ },
110
+ schema: [
111
+ {
112
+ type: "object",
113
+ properties: {
114
+ pairs: {
115
+ type: "array",
116
+ minItems: 1,
117
+ items: {
118
+ type: "object",
119
+ properties: {
120
+ owner: { type: "string", minLength: 1 },
121
+ redundant: { type: "string", minLength: 1 },
122
+ },
123
+ required: ["owner", "redundant"],
124
+ additionalProperties: false,
125
+ },
126
+ },
127
+ },
128
+ required: ["pairs"],
129
+ additionalProperties: false,
130
+ },
131
+ ],
132
+ messages: {
133
+ options: "Configure exact owner/redundant rule pairs whose overlap this consumer has verified.",
134
+ redundant: "{{redundant}} is redundant while {{owner}} is enabled in this rules object. Keep the verified owner.",
135
+ },
136
+ },
137
+ create(context) {
138
+ const options = Array.head(context.options).pipe(Option.flatMap(Schema.decodeUnknownOption(Options)));
139
+ const canonicalFile = /(?:^|\/)oxlint\.config\.[cm]?[jt]s$/u.test(context.filename.replaceAll("\\", "/"));
140
+ const inspect = (node) => {
141
+ Option.match(Option.all([options, staticProperties(context, node)]), {
142
+ onNone: () => { },
143
+ onSome: ([configuration, properties]) => {
144
+ Array.forEach(Array.dedupeWith(configuration.pairs, (left, right) => left.owner === right.owner && left.redundant === right.redundant), ({ owner, redundant }) => {
145
+ if (owner === redundant)
146
+ return;
147
+ const pair = Option.all([
148
+ Record.get(properties, owner),
149
+ Record.get(properties, redundant),
150
+ ]);
151
+ Option.match(pair, {
152
+ onNone: () => { },
153
+ onSome: ([ownerProperty, redundantProperty]) => {
154
+ if (enabled(context, ownerProperty.value) &&
155
+ enabled(context, redundantProperty.value)) {
156
+ context.report({
157
+ node: redundantProperty,
158
+ messageId: "redundant",
159
+ data: { owner, redundant },
160
+ });
161
+ }
162
+ },
163
+ });
164
+ });
165
+ },
166
+ });
167
+ };
168
+ const inspectConfiguration = (node) => {
169
+ Option.match(staticProperties(context, node), {
170
+ onNone: () => { },
171
+ onSome: (properties) => {
172
+ Option.match(Record.get(properties, "rules"), {
173
+ onNone: () => { },
174
+ onSome: (rules) => inspect(rules.value),
175
+ });
176
+ Record.get(properties, "overrides").pipe(Option.flatMap((overrides) => staticValue(context, overrides.value)), Option.filter((value) => value.type === "ArrayExpression"), Option.map((value) => {
177
+ if (value.type === "ArrayExpression")
178
+ Array.forEach(value.elements, (entry) => {
179
+ if (entry)
180
+ inspectConfiguration(entry);
181
+ });
182
+ }));
183
+ },
184
+ });
185
+ };
186
+ return {
187
+ "Program:exit"(program) {
188
+ if (Option.isNone(options)) {
189
+ context.report({ node: program, messageId: "options" });
190
+ return;
191
+ }
192
+ const roots = Array.flatMap(descendants(context, program), (node) => {
193
+ if (node.type === "CallExpression" && configFunction(context, node.callee)) {
194
+ return Option.toArray(Array.head(node.arguments));
195
+ }
196
+ if (canonicalFile && node.type === "ExportDefaultDeclaration")
197
+ return [node.declaration];
198
+ return [];
199
+ });
200
+ Array.forEach(Array.dedupeWith(roots, (left, right) => left === right), inspectConfiguration);
201
+ },
202
+ };
203
+ },
204
+ });
package/dist/index.d.ts CHANGED
@@ -31,6 +31,8 @@ export declare const effect: {
31
31
  "effect/no-manual-tag": "error";
32
32
  "effect/no-raw-fetch": "error";
33
33
  "strict-effect/static-layers": "error";
34
+ "strict-effect/subprocess-pipe-capture": "error";
35
+ "strict-effect/no-runtime-type-assertions": "error";
34
36
  "strict-effect/no-unchecked-index": "error";
35
37
  "strict-effect/no-native-at": "error";
36
38
  "strict-effect/tagged-error-name": "error";
package/dist/index.js CHANGED
@@ -36,6 +36,8 @@ export const effect = {
36
36
  "effect/no-manual-tag": "error",
37
37
  "effect/no-raw-fetch": "error",
38
38
  "strict-effect/static-layers": "error",
39
+ "strict-effect/subprocess-pipe-capture": "error",
40
+ "strict-effect/no-runtime-type-assertions": "error",
39
41
  "strict-effect/no-unchecked-index": "error",
40
42
  "strict-effect/no-native-at": "error",
41
43
  "strict-effect/tagged-error-name": "error",
@@ -0,0 +1,5 @@
1
+ import type { Context, ESTree, Variable } from "@oxlint/plugins";
2
+ import * as Option from "effect/Option";
3
+ export declare const constant: (variable: Variable) => Option.Option<ESTree.VariableDeclarator>;
4
+ export declare const valueOf: (context: Context, node: ESTree.Node, visited?: readonly ESTree.Node[]) => Option.Option<ESTree.Node>;
5
+ export declare const operation: (context: Context, node: ESTree.Node, module: string, name: string) => boolean;
@@ -0,0 +1,87 @@
1
+ import * as Array from "effect/Array";
2
+ import * as Option from "effect/Option";
3
+ import { propertyName, variableInScope } from "./syntax.js";
4
+ export const constant = (variable) => Array.head(variable.defs).pipe(Option.filter(() => variable.defs.length === 1), Option.filter(() => Array.every((reference) => !reference.isWrite() || reference.init)(variable.references)), Option.flatMap((definition) => definition.node.type === "VariableDeclarator" &&
5
+ definition.parent?.type === "VariableDeclaration" &&
6
+ definition.parent.kind === "const"
7
+ ? Option.some(definition.node)
8
+ : Option.none()));
9
+ const member = (reference, name) => {
10
+ if (Option.isSome(reference.name))
11
+ return Option.none();
12
+ return reference.module === "alchemy" ||
13
+ reference.module === "effect" ||
14
+ (reference.module === "effect/process" &&
15
+ (name === "ChildProcess" || name === "ChildProcessSpawner"))
16
+ ? Option.some({ module: `${reference.module}/${name}`, name: Option.none() })
17
+ : Option.some({ ...reference, name: Option.some(name) });
18
+ };
19
+ // Only real imports and immutable local aliases count. The nearest declaration
20
+ // prevents a shadowed parameter from borrowing an outer import's identity.
21
+ const referenceOf = (context, node, visited = []) => {
22
+ if (Array.some(visited, (previous) => previous === node))
23
+ return Option.none();
24
+ const next = [...visited, node];
25
+ if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression") {
26
+ return referenceOf(context, node.expression, next);
27
+ }
28
+ if (node.type === "MemberExpression") {
29
+ const name = node.computed
30
+ ? node.property.type === "Literal"
31
+ ? propertyName(node.property)
32
+ : Option.none()
33
+ : propertyName(node.property);
34
+ return Option.all([referenceOf(context, node.object, next), name]).pipe(Option.flatMap(([reference, key]) => member(reference, key)));
35
+ }
36
+ if (node.type !== "Identifier")
37
+ return Option.none();
38
+ return variableInScope(context.sourceCode.getScope(node), node.name).pipe(Option.flatMap((variable) => Array.head(variable.defs).pipe(Option.flatMap((definition) => {
39
+ if (definition.type === "ImportBinding" &&
40
+ definition.parent?.type === "ImportDeclaration" &&
41
+ definition.parent.importKind !== "type") {
42
+ const module = definition.parent.source.value;
43
+ if (module !== "alchemy" &&
44
+ !module.startsWith("alchemy/") &&
45
+ module !== "effect" &&
46
+ !module.startsWith("effect/"))
47
+ return Option.none();
48
+ const reference = { module, name: Option.none() };
49
+ if (definition.node.type === "ImportNamespaceSpecifier")
50
+ return Option.some(reference);
51
+ if (definition.node.type === "ImportSpecifier" &&
52
+ definition.node.importKind !== "type") {
53
+ return propertyName(definition.node.imported).pipe(Option.flatMap((name) => member(reference, name)));
54
+ }
55
+ return Option.none();
56
+ }
57
+ return constant(variable).pipe(Option.flatMap((declaration) => Option.fromNullishOr(declaration.init).pipe(Option.flatMap((initialiser) => {
58
+ if (declaration.id.type === "Identifier") {
59
+ return referenceOf(context, initialiser, next);
60
+ }
61
+ if (declaration.id.type !== "ObjectPattern")
62
+ return Option.none();
63
+ return Array.findFirst((property) => property.type === "Property" &&
64
+ property.value.type === "Identifier" &&
65
+ property.value.name === node.name &&
66
+ !property.computed)(declaration.id.properties).pipe(Option.flatMap((property) => property.type === "Property"
67
+ ? Option.all([
68
+ referenceOf(context, initialiser, next),
69
+ propertyName(property.key),
70
+ ]).pipe(Option.flatMap(([reference, key]) => member(reference, key)))
71
+ : Option.none()));
72
+ }))));
73
+ }))));
74
+ };
75
+ export const valueOf = (context, node, visited = []) => {
76
+ if (Array.some(visited, (previous) => previous === node))
77
+ return Option.none();
78
+ const next = [...visited, node];
79
+ if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression") {
80
+ return valueOf(context, node.expression, next);
81
+ }
82
+ if (node.type !== "Identifier")
83
+ return Option.some(node);
84
+ return variableInScope(context.sourceCode.getScope(node), node.name).pipe(Option.flatMap(constant), Option.filter((declaration) => declaration.id.type === "Identifier"), Option.flatMap((declaration) => Option.fromNullishOr(declaration.init)), Option.flatMap((initialiser) => valueOf(context, initialiser, next)));
85
+ };
86
+ export const operation = (context, node, module, name) => referenceOf(context, node).pipe(Option.exists((reference) => (reference.module === module && Option.contains(reference.name, name)) ||
87
+ (reference.module === `${module}/${name}` && Option.isNone(reference.name))));
@@ -0,0 +1 @@
1
+ export declare const workspaceImportBoundariesRule: import("@oxlint/plugins").Rule;