@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.
- package/LICENSE +21 -0
- package/README.md +130 -2
- package/dist/alchemy.d.ts +2 -0
- package/dist/alchemy.js +196 -0
- package/dist/anti-slop/effect/index.d.ts +3 -0
- package/dist/anti-slop/effect/index.js +10 -0
- package/dist/anti-slop/effect/rules/no-service-constructor-imports.d.ts +2 -0
- package/dist/anti-slop/effect/rules/no-service-constructor-imports.js +45 -0
- package/dist/anti-slop/index.d.ts +3 -0
- package/dist/anti-slop/index.js +38 -0
- package/dist/anti-slop/rules/no-chained-type-assertions.d.ts +2 -0
- package/dist/anti-slop/rules/no-chained-type-assertions.js +57 -0
- package/dist/anti-slop/rules/no-conditional-empty-object-spread.d.ts +2 -0
- package/dist/anti-slop/rules/no-conditional-empty-object-spread.js +36 -0
- package/dist/anti-slop/rules/no-known-value-widening.d.ts +2 -0
- package/dist/anti-slop/rules/no-known-value-widening.js +171 -0
- package/dist/anti-slop/rules/no-module-mocking.d.ts +2 -0
- package/dist/anti-slop/rules/no-module-mocking.js +73 -0
- package/dist/anti-slop/rules/no-object-parameters.d.ts +2 -0
- package/dist/anti-slop/rules/no-object-parameters.js +86 -0
- package/dist/anti-slop/rules/no-reflect-apply.d.ts +2 -0
- package/dist/anti-slop/rules/no-reflect-apply.js +25 -0
- package/dist/anti-slop/rules/no-reflect-get.d.ts +2 -0
- package/dist/anti-slop/rules/no-reflect-get.js +25 -0
- package/dist/anti-slop/rules/no-runtime-typeof.d.ts +2 -0
- package/dist/anti-slop/rules/no-runtime-typeof.js +47 -0
- package/dist/anti-slop/rules/no-shape-in-symbol-names.d.ts +2 -0
- package/dist/anti-slop/rules/no-shape-in-symbol-names.js +33 -0
- package/dist/anti-slop/rules/no-unknown-parameters.d.ts +2 -0
- package/dist/anti-slop/rules/no-unknown-parameters.js +69 -0
- package/dist/anti-slop/rules/no-unknown-returns.d.ts +2 -0
- package/dist/anti-slop/rules/no-unknown-returns.js +77 -0
- package/dist/anti-slop/rules/no-unknown-type-aliases.d.ts +2 -0
- package/dist/anti-slop/rules/no-unknown-type-aliases.js +58 -0
- package/dist/anti-slop/rules/no-unsafe-dictionary-type.d.ts +2 -0
- package/dist/anti-slop/rules/no-unsafe-dictionary-type.js +111 -0
- package/dist/anti-slop/rules/no-widen-then-assert.d.ts +2 -0
- package/dist/anti-slop/rules/no-widen-then-assert.js +255 -0
- package/dist/anti-slop/rules/require-safety-comment-for-type-assertion.d.ts +2 -0
- package/dist/anti-slop/rules/require-safety-comment-for-type-assertion.js +51 -0
- package/dist/anti-slop/shared/dictionary-types.d.ts +21 -0
- package/dist/anti-slop/shared/dictionary-types.js +374 -0
- package/dist/anti-slop/shared/lexical-type-parameters.d.ts +3 -0
- package/dist/anti-slop/shared/lexical-type-parameters.js +25 -0
- package/dist/anti-slop/shared/reflect-method.d.ts +3 -0
- package/dist/anti-slop/shared/reflect-method.js +24 -0
- package/dist/bun.d.ts +9 -0
- package/dist/bun.js +30 -0
- package/dist/effect.d.ts +17 -0
- package/dist/effect.js +352 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +62 -0
- package/dist/package.d.ts +9 -0
- package/dist/package.js +50 -0
- package/dist/strict-effect.d.ts +2 -0
- package/dist/strict-effect.js +2381 -0
- package/dist/syntax.d.ts +5 -0
- package/dist/syntax.js +26 -0
- 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
|
-
#
|
|
1
|
+
# @commonplaces/oxlint-plugin
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
package/dist/alchemy.js
ADDED
|
@@ -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,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,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,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,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,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
|
+
});
|