@commonplaces/oxlint-plugin 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +130 -2
  3. package/dist/alchemy.d.ts +2 -0
  4. package/dist/alchemy.js +196 -0
  5. package/dist/anti-slop/effect/index.d.ts +3 -0
  6. package/dist/anti-slop/effect/index.js +10 -0
  7. package/dist/anti-slop/effect/rules/no-service-constructor-imports.d.ts +2 -0
  8. package/dist/anti-slop/effect/rules/no-service-constructor-imports.js +45 -0
  9. package/dist/anti-slop/index.d.ts +3 -0
  10. package/dist/anti-slop/index.js +38 -0
  11. package/dist/anti-slop/rules/no-chained-type-assertions.d.ts +2 -0
  12. package/dist/anti-slop/rules/no-chained-type-assertions.js +57 -0
  13. package/dist/anti-slop/rules/no-conditional-empty-object-spread.d.ts +2 -0
  14. package/dist/anti-slop/rules/no-conditional-empty-object-spread.js +36 -0
  15. package/dist/anti-slop/rules/no-known-value-widening.d.ts +2 -0
  16. package/dist/anti-slop/rules/no-known-value-widening.js +171 -0
  17. package/dist/anti-slop/rules/no-module-mocking.d.ts +2 -0
  18. package/dist/anti-slop/rules/no-module-mocking.js +73 -0
  19. package/dist/anti-slop/rules/no-object-parameters.d.ts +2 -0
  20. package/dist/anti-slop/rules/no-object-parameters.js +86 -0
  21. package/dist/anti-slop/rules/no-reflect-apply.d.ts +2 -0
  22. package/dist/anti-slop/rules/no-reflect-apply.js +25 -0
  23. package/dist/anti-slop/rules/no-reflect-get.d.ts +2 -0
  24. package/dist/anti-slop/rules/no-reflect-get.js +25 -0
  25. package/dist/anti-slop/rules/no-runtime-typeof.d.ts +2 -0
  26. package/dist/anti-slop/rules/no-runtime-typeof.js +47 -0
  27. package/dist/anti-slop/rules/no-shape-in-symbol-names.d.ts +2 -0
  28. package/dist/anti-slop/rules/no-shape-in-symbol-names.js +33 -0
  29. package/dist/anti-slop/rules/no-unknown-parameters.d.ts +2 -0
  30. package/dist/anti-slop/rules/no-unknown-parameters.js +69 -0
  31. package/dist/anti-slop/rules/no-unknown-returns.d.ts +2 -0
  32. package/dist/anti-slop/rules/no-unknown-returns.js +77 -0
  33. package/dist/anti-slop/rules/no-unknown-type-aliases.d.ts +2 -0
  34. package/dist/anti-slop/rules/no-unknown-type-aliases.js +58 -0
  35. package/dist/anti-slop/rules/no-unsafe-dictionary-type.d.ts +2 -0
  36. package/dist/anti-slop/rules/no-unsafe-dictionary-type.js +111 -0
  37. package/dist/anti-slop/rules/no-widen-then-assert.d.ts +2 -0
  38. package/dist/anti-slop/rules/no-widen-then-assert.js +255 -0
  39. package/dist/anti-slop/rules/require-safety-comment-for-type-assertion.d.ts +2 -0
  40. package/dist/anti-slop/rules/require-safety-comment-for-type-assertion.js +51 -0
  41. package/dist/anti-slop/shared/dictionary-types.d.ts +21 -0
  42. package/dist/anti-slop/shared/dictionary-types.js +374 -0
  43. package/dist/anti-slop/shared/lexical-type-parameters.d.ts +3 -0
  44. package/dist/anti-slop/shared/lexical-type-parameters.js +25 -0
  45. package/dist/anti-slop/shared/reflect-method.d.ts +3 -0
  46. package/dist/anti-slop/shared/reflect-method.js +24 -0
  47. package/dist/bun.d.ts +9 -0
  48. package/dist/bun.js +30 -0
  49. package/dist/effect.d.ts +17 -0
  50. package/dist/effect.js +352 -0
  51. package/dist/index.d.ts +57 -0
  52. package/dist/index.js +62 -0
  53. package/dist/package.d.ts +9 -0
  54. package/dist/package.js +50 -0
  55. package/dist/strict-effect.d.ts +2 -0
  56. package/dist/strict-effect.js +2381 -0
  57. package/dist/syntax.d.ts +5 -0
  58. package/dist/syntax.js +26 -0
  59. package/package.json +77 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cooper Corbett
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,131 @@
1
- # Temporary Holding Version
1
+ # @commonplaces/oxlint-plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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**.
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.
8
+
9
+ ## Install
10
+
11
+ Install from npm:
12
+
13
+ ```sh
14
+ npm install --save-dev --save-exact @commonplaces/oxlint-plugin@0.1.1 oxlint@1.80.0
15
+ ```
16
+
17
+ Create `oxlint.config.ts`:
18
+
19
+ ```ts
20
+ import { defineConfig } from "oxlint";
21
+ import { general, effect, alchemy } from "@commonplaces/oxlint-plugin";
22
+
23
+ export default defineConfig({
24
+ extends: [general, effect, alchemy],
25
+ });
26
+ ```
27
+
28
+ Then run `npx oxlint .`. Choose only the groups that fit your project. `general`
29
+ checks code style and package imports; `effect` checks Effect code; `alchemy`
30
+ checks native Alchemy settings and secret sources. Each group turns its checks
31
+ on as errors. They do not depend on one another. Override individual rules in
32
+ your own `rules` or `overrides` settings.
33
+
34
+ The root export contains these three configurations. Individual plugins use
35
+ the subpaths below. For example, a JSON configuration can enable one rule:
36
+
37
+ ```json
38
+ {
39
+ "jsPlugins": ["@commonplaces/oxlint-plugin/strict-effect"],
40
+ "rules": { "strict-effect/static-layers": "error" }
41
+ }
42
+ ```
43
+
44
+ ## Project settings
45
+
46
+ Rules that refer to project files, services, schemas or routes are available
47
+ but excluded from the groups. Configure them explicitly, for example:
48
+
49
+ ```ts
50
+ import { defineConfig } from "oxlint";
51
+ import { general, effect } from "@commonplaces/oxlint-plugin";
52
+
53
+ export default defineConfig({
54
+ extends: [general, effect],
55
+ rules: {
56
+ "effect/no-runtime-execution-outside-main": [
57
+ "error",
58
+ {
59
+ allowedFiles: ["apps/cli/src/main.ts"],
60
+ },
61
+ ],
62
+ "effect/require-service-operation-instrumentation": [
63
+ "error",
64
+ {
65
+ serviceNames: ["Orders"],
66
+ },
67
+ ],
68
+ "strict-effect/module-ownership": [
69
+ "error",
70
+ {
71
+ owners: [
72
+ {
73
+ module: "vendor-sdk",
74
+ allowedFiles: ["packages/orders/src/live.layer.ts"],
75
+ allowTypes: false,
76
+ },
77
+ ],
78
+ },
79
+ ],
80
+ },
81
+ });
82
+ ```
83
+
84
+ Use repository-relative paths with `/` separators. Runtime execution is denied
85
+ everywhere unless `allowedFiles` grants an entry file. The service tracing rule
86
+ requires `serviceNames` and checks names such as `Effect.fn("Orders.create")`.
87
+ Invalid settings stop linting. Each rule's exported `meta.schema` describes its
88
+ accepted options; no marketplace plugin is needed to read or run them.
89
+
90
+ The portable Effect checks use the existing `strict-effect/` names, and the
91
+ Alchemy checks use `alchemy-secrets/`. Keep those names when moving from copied
92
+ rule files. Remove the copied implementations and change `jsPlugins` to the
93
+ package subpaths; keep your existing rule settings.
94
+
95
+ ## Rule exports
96
+
97
+ | Package subpath | Rule prefix | Rules |
98
+ | ------------------- | ------------------- | ----: |
99
+ | `/strict-effect` | `strict-effect/` | 27 |
100
+ | `/effect` | `effect/` | 9 |
101
+ | `/alchemy` | `alchemy-secrets/` | 3 |
102
+ | `/bun` | `bun/` | 1 |
103
+ | `/package` | `package/` | 1 |
104
+ | `/anti-slop` | `anti-slop/` | 15 |
105
+ | `/anti-slop-effect` | `anti-slop-effect/` | 1 |
106
+
107
+ ## Development
108
+
109
+ This workspace owns the rule code. Skills provide guidance and installation
110
+ instructions; they do not carry another copy of the rules. The package contains
111
+ compiled JavaScript and declarations, the README and the MIT licence. It does
112
+ not contain the private marketplace's skills, icons, source maps or tests.
113
+
114
+ Run `bun run build:lint-plugin` from the repository root before linting. Root
115
+ installation and lint/test commands do this automatically. Local consumers and
116
+ npm consumers use the same compiled exports; there is no source-only export
117
+ condition to hide packaging mistakes. Run `bun run test:lint-rules` for existing
118
+ integration checks and `bun run test` for the archive installation
119
+ and public contract checks.
120
+
121
+ Oxlint owns synchronous rule callbacks and reporting. Pure checks use immutable
122
+ Effect collections. The two synchronous option decoders in `effect.ts` are
123
+ required by Oxlint's `create` callback and throw only for invalid configuration.
124
+ The `syntax.ts` child adapter selects nodes from Oxlint's already parsed tree
125
+ using its visitor keys; it is not a decoder for external JSON. Traversal cycle
126
+ checks compare host nodes and variables by reference, never structural equality. Native nullable
127
+ syntax fields stay at this host boundary. There is no Effect runtime, service
128
+ or infrastructure stack inside the package.
129
+
130
+ The strict Effect rules were adapted from Cooper's DAW rules at `e47ecfb` and
131
+ subsequent Commonplace rules. The package's MIT licence covers this lint code.
@@ -0,0 +1,2 @@
1
+ declare const _default: import("@oxlint/plugins").Plugin;
2
+ export default _default;
@@ -0,0 +1,196 @@
1
+ import { defineRule, eslintCompatPlugin } from "@oxlint/plugins";
2
+ import * as Array from "effect/Array";
3
+ import * as Option from "effect/Option";
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))));
96
+ 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
+ const field = (node, name) => node.type === "ObjectExpression" &&
98
+ Array.every((property) => property.type === "Property" && property.kind === "init" && !property.computed)(node.properties)
99
+ ? Array.findLast((property) => property.type === "Property" && Option.contains(propertyName(property.key), name))(node.properties).pipe(Option.filter((property) => property.type === "Property"))
100
+ : Option.none();
101
+ const shellDisabled = (context, node) => providerCall(context, node, "alchemy/Secrets", "ProcessEnv").pipe(Option.flatMap((call) => Array.head(call.arguments)), Option.flatMap((argument) => valueOf(context, argument)), Option.flatMap((options) => field(options, "disabled")), Option.flatMap((property) => valueOf(context, property.value)), Option.exists((disabled) => disabled.type === "Literal" && disabled.value === true));
102
+ const nativeSources = defineRule({
103
+ meta: {
104
+ type: "problem",
105
+ schema: [],
106
+ messages: {
107
+ sources: "Use [Doppler.Secrets(...), Alchemy.Secrets.DotEnv(), Alchemy.Secrets.ProcessEnv({ disabled: true })] in every stage; .env overrides Doppler.",
108
+ shell: "Disable shell settings with Alchemy.Secrets.ProcessEnv({ disabled: true }); provider sign-in remains Alchemy's native responsibility.",
109
+ },
110
+ },
111
+ create(context) {
112
+ return {
113
+ ArrayExpression(node) {
114
+ const doppler = (element) => Option.isSome(providerCall(context, element, "alchemy/Doppler", "Secrets"));
115
+ if (!Array.some(node.elements, (element) => element !== null && doppler(element)))
116
+ return;
117
+ const valid = node.elements.length === 3 &&
118
+ Array.get(node.elements, 0).pipe(Option.flatMap(Option.fromNullishOr), Option.exists(doppler)) &&
119
+ Array.get(node.elements, 1).pipe(Option.flatMap(Option.fromNullishOr), Option.exists((element) => Option.isSome(providerCall(context, element, "alchemy/Secrets", "DotEnv")))) &&
120
+ Array.get(node.elements, 2).pipe(Option.flatMap(Option.fromNullishOr), Option.exists((element) => shellDisabled(context, element)));
121
+ if (!valid)
122
+ context.report({ node, messageId: "sources" });
123
+ },
124
+ CallExpression(node) {
125
+ if (operation(context, node.callee, "alchemy/Secrets", "ProcessEnv") &&
126
+ !shellDisabled(context, node)) {
127
+ context.report({ node, messageId: "shell" });
128
+ }
129
+ },
130
+ };
131
+ },
132
+ });
133
+ const explicitStackSources = defineRule({
134
+ meta: {
135
+ type: "problem",
136
+ schema: [],
137
+ messages: {
138
+ stack: "Declare secrets explicitly on the native Stack using the shared checked provider list; omitted settings keep Alchemy's legacy shell/dotenv behaviour.",
139
+ },
140
+ },
141
+ create(context) {
142
+ return {
143
+ CallExpression(node) {
144
+ // Zero-argument and name-only calls declare a Stack class, not a configured stack.
145
+ if (node.arguments.length < 2 || !operation(context, node.callee, "alchemy", "Stack"))
146
+ return;
147
+ const explicit = Array.get(node.arguments, 1).pipe(Option.flatMap((argument) => valueOf(context, argument)), Option.flatMap((options) => field(options, "secrets")), Option.exists((property) => property.value.type !== "Literal" &&
148
+ !(property.value.type === "Identifier" && property.value.name === "undefined") &&
149
+ valueOf(context, property.value).pipe(Option.match({
150
+ onNone: () => true,
151
+ onSome: (value) => value.type !== "Literal" &&
152
+ (value.type !== "ArrayExpression" || value.elements.length === 3),
153
+ }))));
154
+ if (!explicit)
155
+ context.report({ node, messageId: "stack" });
156
+ },
157
+ };
158
+ },
159
+ });
160
+ const commandText = (context, node) => valueOf(context, node).pipe(Option.flatMap((value) => value.type === "Literal" && Predicate.isString(value.value)
161
+ ? Option.some(value.value)
162
+ : Option.none()));
163
+ const noDopplerWrapper = defineRule({
164
+ meta: {
165
+ type: "problem",
166
+ schema: [],
167
+ messages: {
168
+ wrapper: "Run the native Alchemy CLI with its declared secrets; remove Doppler CLI injection from this command.",
169
+ },
170
+ },
171
+ create(context) {
172
+ return {
173
+ CallExpression(node) {
174
+ if (!operation(context, node.callee, "effect/process/ChildProcess", "make"))
175
+ return;
176
+ const command = Array.head(node.arguments).pipe(Option.flatMap((argument) => commandText(context, argument)));
177
+ const argumentsList = Array.get(node.arguments, 1).pipe(Option.flatMap((argument) => valueOf(context, argument)), Option.filter((argument) => argument.type === "ArrayExpression"));
178
+ const direct = Option.exists(command, (text) => /(?:^|\/)doppler$/u.test(text)) &&
179
+ argumentsList.pipe(Option.flatMap((argumentsNode) => Array.head(argumentsNode.elements)), Option.flatMap(Option.fromNullishOr), Option.flatMap((argument) => commandText(context, argument)), Option.contains("run"));
180
+ const shell = Option.exists(command, (text) => /(?:^|\/)(?:sh|bash|zsh)$/u.test(text)) &&
181
+ Option.exists(argumentsList, (argumentsNode) => Array.some(argumentsNode.elements, (argument) => argument !== null &&
182
+ Option.exists(commandText(context, argument), (text) => /(?:^|[\s;|&])doppler\s+run(?:\s|$)/u.test(text))));
183
+ if (direct || shell)
184
+ context.report({ node, messageId: "wrapper" });
185
+ },
186
+ };
187
+ },
188
+ });
189
+ export default eslintCompatPlugin({
190
+ meta: { name: "alchemy-secrets" },
191
+ rules: {
192
+ "native-sources": nativeSources,
193
+ "explicit-stack-sources": explicitStackSources,
194
+ "no-doppler-wrapper": noDopplerWrapper,
195
+ },
196
+ });
@@ -0,0 +1,3 @@
1
+ /** Opt-in Oxlint rules for Effect service and Layer architecture. */
2
+ declare const antiSlopEffectPlugin: import("@oxlint/plugins").Plugin;
3
+ export default antiSlopEffectPlugin;
@@ -0,0 +1,10 @@
1
+ import { eslintCompatPlugin } from "@oxlint/plugins";
2
+ import { noServiceConstructorImportsRule } from "./rules/no-service-constructor-imports.js";
3
+ /** Opt-in Oxlint rules for Effect service and Layer architecture. */
4
+ const antiSlopEffectPlugin = eslintCompatPlugin({
5
+ meta: { name: "anti-slop-effect" },
6
+ rules: {
7
+ "no-service-constructor-imports": noServiceConstructorImportsRule,
8
+ },
9
+ });
10
+ export default antiSlopEffectPlugin;
@@ -0,0 +1,2 @@
1
+ /** Keep dependency-bearing Effect service constructors local to their owning capability modules. */
2
+ export declare const noServiceConstructorImportsRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,45 @@
1
+ import { Array } from "effect";
2
+ import { defineRule } from "@oxlint/plugins";
3
+ const SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u;
4
+ const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
5
+ function isProjectLocalImport(source) {
6
+ return source.startsWith("./") || source.startsWith("../");
7
+ }
8
+ function getImportedName(specifier) {
9
+ if (specifier.imported.type === "Identifier")
10
+ return specifier.imported.name;
11
+ return specifier.imported.value;
12
+ }
13
+ /** Keep dependency-bearing Effect service constructors local to their owning capability modules. */
14
+ export const noServiceConstructorImportsRule = defineRule({
15
+ meta: {
16
+ type: "problem",
17
+ docs: {
18
+ description: "Disallow project-local make<CapabilityName> imports outside test and spec files.",
19
+ },
20
+ messages: {
21
+ 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.',
22
+ },
23
+ },
24
+ create(context) {
25
+ const isTestFile = TEST_FILE.test(context.filename.replaceAll("\\", "/"));
26
+ return {
27
+ ImportDeclaration(node) {
28
+ if (isTestFile || !isProjectLocalImport(node.source.value))
29
+ return;
30
+ Array.forEach(node.specifiers, (specifier) => {
31
+ if (specifier.type !== "ImportSpecifier")
32
+ return;
33
+ const importedName = getImportedName(specifier);
34
+ if (!SERVICE_CONSTRUCTOR_NAME.test(importedName))
35
+ return;
36
+ context.report({
37
+ node: specifier,
38
+ messageId: "serviceConstructorImport",
39
+ data: { name: importedName },
40
+ });
41
+ });
42
+ },
43
+ };
44
+ },
45
+ });
@@ -0,0 +1,3 @@
1
+ /** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
2
+ declare const antiSlopPlugin: import("@oxlint/plugins").Plugin;
3
+ export default antiSlopPlugin;
@@ -0,0 +1,38 @@
1
+ import { eslintCompatPlugin } from "@oxlint/plugins";
2
+ import { noChainedTypeAssertionsRule } from "./rules/no-chained-type-assertions.js";
3
+ import { noConditionalEmptyObjectSpreadRule } from "./rules/no-conditional-empty-object-spread.js";
4
+ import { noKnownValueWideningRule } from "./rules/no-known-value-widening.js";
5
+ import { noModuleMockingRule } from "./rules/no-module-mocking.js";
6
+ import { noObjectParametersRule } from "./rules/no-object-parameters.js";
7
+ import { noReflectApplyRule } from "./rules/no-reflect-apply.js";
8
+ import { noReflectGetRule } from "./rules/no-reflect-get.js";
9
+ import { noRuntimeTypeofRule } from "./rules/no-runtime-typeof.js";
10
+ import { noForbiddenTermInSymbolNamesRule } from "./rules/no-shape-in-symbol-names.js";
11
+ import { noUnknownParametersRule } from "./rules/no-unknown-parameters.js";
12
+ import { noUnknownReturnsRule } from "./rules/no-unknown-returns.js";
13
+ import { noUnknownTypeAliasesRule } from "./rules/no-unknown-type-aliases.js";
14
+ import { noUnsafeDictionaryTypeRule } from "./rules/no-unsafe-dictionary-type.js";
15
+ import { noWidenThenAssertRule } from "./rules/no-widen-then-assert.js";
16
+ import { requireSafetyCommentForTypeAssertionRule } from "./rules/require-safety-comment-for-type-assertion.js";
17
+ /** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
18
+ const antiSlopPlugin = eslintCompatPlugin({
19
+ meta: { name: "anti-slop" },
20
+ rules: {
21
+ "no-chained-type-assertions": noChainedTypeAssertionsRule,
22
+ "no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
23
+ "no-known-value-widening": noKnownValueWideningRule,
24
+ "no-module-mocking": noModuleMockingRule,
25
+ "no-object-parameters": noObjectParametersRule,
26
+ "no-reflect-apply": noReflectApplyRule,
27
+ "no-reflect-get": noReflectGetRule,
28
+ "no-runtime-typeof": noRuntimeTypeofRule,
29
+ "no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
30
+ "no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule,
31
+ "no-unknown-parameters": noUnknownParametersRule,
32
+ "no-unknown-returns": noUnknownReturnsRule,
33
+ "no-unknown-type-aliases": noUnknownTypeAliasesRule,
34
+ "no-widen-then-assert": noWidenThenAssertRule,
35
+ "require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule,
36
+ },
37
+ });
38
+ export default antiSlopPlugin;
@@ -0,0 +1,2 @@
1
+ /** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */
2
+ export declare const noChainedTypeAssertionsRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,57 @@
1
+ import { Array } from "effect";
2
+ import { defineRule } from "@oxlint/plugins";
3
+ function isTypeAssertionExpression(node) {
4
+ return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
5
+ }
6
+ function unwrapParenthesizedExpression(expression) {
7
+ return expression.type === "ParenthesizedExpression"
8
+ ? unwrapParenthesizedExpression(expression.expression)
9
+ : expression;
10
+ }
11
+ function isConstAssertion(node) {
12
+ const { typeAnnotation } = node;
13
+ return (typeAnnotation.type === "TSTypeReference" &&
14
+ typeAnnotation.typeName.type === "Identifier" &&
15
+ typeAnnotation.typeName.name === "const");
16
+ }
17
+ function outsideParentheses(node) {
18
+ return node.parent.type === "ParenthesizedExpression" && node.parent.expression === node
19
+ ? outsideParentheses(node.parent)
20
+ : node;
21
+ }
22
+ function isOutermostAssertionInChain(node) {
23
+ const current = outsideParentheses(node);
24
+ return !isTypeAssertionExpression(current.parent) || current.parent.expression !== current;
25
+ }
26
+ function assertionChain(node) {
27
+ return isTypeAssertionExpression(node)
28
+ ? [node, ...assertionChain(unwrapParenthesizedExpression(node.expression))]
29
+ : [];
30
+ }
31
+ function isForbiddenAssertionChain(node) {
32
+ const chain = assertionChain(node);
33
+ return chain.length > 1 && Array.some(chain, (assertion) => !isConstAssertion(assertion));
34
+ }
35
+ /** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */
36
+ export const noChainedTypeAssertionsRule = defineRule({
37
+ meta: {
38
+ type: "problem",
39
+ docs: {
40
+ description: "Disallow chained TypeScript as and angle-bracket assertions, including parenthesized chains.",
41
+ },
42
+ messages: {
43
+ chained: "This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it.",
44
+ },
45
+ },
46
+ createOnce(context) {
47
+ const checkTypeAssertion = (node) => {
48
+ if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node))
49
+ return;
50
+ context.report({ node, messageId: "chained" });
51
+ };
52
+ return {
53
+ TSAsExpression: checkTypeAssertion,
54
+ TSTypeAssertion: checkTypeAssertion,
55
+ };
56
+ },
57
+ });
@@ -0,0 +1,2 @@
1
+ /** Ban conditional empty-object spreads without changing their omission semantics. */
2
+ export declare const noConditionalEmptyObjectSpreadRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,36 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ function unwrapParentheses(node) {
3
+ return node.type === "ParenthesizedExpression" ? unwrapParentheses(node.expression) : node;
4
+ }
5
+ function isEmptyObjectExpression(node) {
6
+ return node.type === "ObjectExpression" && node.properties.length === 0;
7
+ }
8
+ function isConditionalEmptyObjectSpread(node) {
9
+ const conditional = unwrapParentheses(node);
10
+ return (conditional.type === "ConditionalExpression" &&
11
+ (isEmptyObjectExpression(conditional.consequent) ||
12
+ isEmptyObjectExpression(conditional.alternate)));
13
+ }
14
+ /** Ban conditional empty-object spreads without changing their omission semantics. */
15
+ export const noConditionalEmptyObjectSpreadRule = defineRule({
16
+ meta: {
17
+ type: "suggestion",
18
+ docs: {
19
+ description: "Disallow object spreads that conditionally spread an empty object to omit fields.",
20
+ },
21
+ messages: {
22
+ avoid: "This conditional spread hides property omission behind an empty object. Build the object in separate statements and add the property only when present.",
23
+ },
24
+ },
25
+ createOnce(context) {
26
+ return {
27
+ SpreadElement(node) {
28
+ if (node.parent.type !== "ObjectExpression")
29
+ return;
30
+ if (isConditionalEmptyObjectSpread(node.argument)) {
31
+ context.report({ node, messageId: "avoid" });
32
+ }
33
+ },
34
+ };
35
+ },
36
+ });
@@ -0,0 +1,2 @@
1
+ /** Detect sound syntactic cases where a known value is explicitly widened and loses evidence. */
2
+ export declare const noKnownValueWideningRule: import("@oxlint/plugins").Rule;