oxlint-plugin-effector 0.0.1 → 0.0.3
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 +20 -6
- package/dist/index.js +96 -11
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -12,9 +12,11 @@ checker and passes it.
|
|
|
12
12
|
The Effector rules need TypeScript types (to tell a `Store` from an `Event`, follow units across files, etc.).
|
|
13
13
|
oxlint does not hand type information to JS plugins. So instead of relying on it, **the plugin builds its own
|
|
14
14
|
`ts.Program`** using the `typescript` package directly and calls `getTypeAtLocation` itself, mapping oxlint's AST
|
|
15
|
-
nodes to TS nodes by source range.
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
nodes to TS nodes by source range. This is an oxlint plugin — it does **not** target or support ESLint; under
|
|
16
|
+
oxlint the plugin always constructs its own program. There is **no `@typescript-eslint` in the runtime** — the
|
|
17
|
+
only runtime dependencies are `typescript` (peer) and `esquery`. (The test suite happens to run the rules through
|
|
18
|
+
`@typescript-eslint/rule-tester` to validate against the real type checker, but that's a testing detail, not a
|
|
19
|
+
runtime target.)
|
|
18
20
|
|
|
19
21
|
Program discovery (`src/shared/services.ts`) is on plain `typescript` and handles the awkward setups:
|
|
20
22
|
|
|
@@ -23,9 +25,21 @@ Program discovery (`src/shared/services.ts`) is on plain `typescript` and handle
|
|
|
23
25
|
- if no tsconfig is found, a node-resolution fallback is used so `effector`/`react` types still resolve;
|
|
24
26
|
- set **`OXLINT_EFFECTOR_TSCONFIG`** to point at a specific tsconfig (custom names, monorepo roots).
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
### Performance
|
|
29
|
+
|
|
30
|
+
These rules are **type-aware**, so each oxlint worker thread builds a TypeScript program for the project and
|
|
31
|
+
resolves types — the same fixed cost any type-aware linter pays (comparable to `typescript-eslint` with
|
|
32
|
+
`parserOptions.project`). On a real ~350-file React project expect a few seconds of overhead over oxlint's native
|
|
33
|
+
rules; it is dominated by the one-time program build + type resolution, which parallelizes across threads and is
|
|
34
|
+
largely inherent (it cannot be shared with oxlint's own type checker). Functionally there are **no limitations**
|
|
35
|
+
versus `eslint-plugin-effector` (the full upstream test suite — 349 cases — passes against the real type checker).
|
|
36
|
+
|
|
37
|
+
Levers if it's too slow:
|
|
38
|
+
|
|
39
|
+
- **`OXLINT_EFFECTOR_TSCONFIG`** → point at a lean tsconfig that `include`s only your source (skip tests/build
|
|
40
|
+
output) so the program is smaller;
|
|
41
|
+
- **`oxlint --threads N`** → on a type-aware run, fewer threads can be as fast or faster (each thread builds its
|
|
42
|
+
own program), so try a lower number than your core count.
|
|
29
43
|
|
|
30
44
|
## Installation
|
|
31
45
|
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,24 @@ var NodeType = {
|
|
|
19
19
|
SpreadElement: "SpreadElement",
|
|
20
20
|
VariableDeclarator: "VariableDeclarator"
|
|
21
21
|
};
|
|
22
|
+
var NON_UNIT_INIT = /* @__PURE__ */ new Set([
|
|
23
|
+
"ArrayExpression",
|
|
24
|
+
"ArrowFunctionExpression",
|
|
25
|
+
"BinaryExpression",
|
|
26
|
+
"ClassExpression",
|
|
27
|
+
"FunctionExpression",
|
|
28
|
+
"JSXElement",
|
|
29
|
+
"JSXFragment",
|
|
30
|
+
"Literal",
|
|
31
|
+
"ObjectExpression",
|
|
32
|
+
"TaggedTemplateExpression",
|
|
33
|
+
"TemplateLiteral",
|
|
34
|
+
"UnaryExpression"
|
|
35
|
+
]);
|
|
36
|
+
var bindingCannotBeUnit = (node) => {
|
|
37
|
+
const parent = node.parent;
|
|
38
|
+
return parent?.type === NodeType.VariableDeclarator && parent.id === node && parent.init != null && NON_UNIT_INIT.has(parent.init.type);
|
|
39
|
+
};
|
|
22
40
|
|
|
23
41
|
// src/shared/services.ts
|
|
24
42
|
import { dirname, resolve } from "node:path";
|
|
@@ -26,22 +44,33 @@ import * as ts from "typescript";
|
|
|
26
44
|
function getParserServices(context) {
|
|
27
45
|
const existing = context.sourceCode?.parserServices;
|
|
28
46
|
if (existing?.program && existing.esTreeNodeToTSNodeMap) return existing;
|
|
29
|
-
|
|
47
|
+
const filename = context.filename ?? context.physicalFilename;
|
|
48
|
+
let services = servicesCache.get(filename);
|
|
49
|
+
if (!services) {
|
|
50
|
+
services = buildServices(context);
|
|
51
|
+
servicesCache.set(filename, services);
|
|
52
|
+
}
|
|
53
|
+
return services;
|
|
30
54
|
}
|
|
55
|
+
var servicesCache = /* @__PURE__ */ new Map();
|
|
31
56
|
var programCache = /* @__PURE__ */ new Map();
|
|
57
|
+
var standaloneCache = /* @__PURE__ */ new Map();
|
|
58
|
+
var findSourceFile = (program, filename) => program.getSourceFile(filename) ?? program.getSourceFiles().find((sf) => norm(sf.fileName) === norm(filename));
|
|
32
59
|
function buildServices(context) {
|
|
33
60
|
const filename = context.filename ?? context.physicalFilename;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (!sourceFile)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
61
|
+
let program = getProgram(filename);
|
|
62
|
+
let sourceFile = findSourceFile(program, filename);
|
|
63
|
+
if (!sourceFile) {
|
|
64
|
+
program = getStandaloneProgram(filename);
|
|
65
|
+
sourceFile = findSourceFile(program, filename);
|
|
66
|
+
}
|
|
67
|
+
if (!sourceFile) return getInertServices();
|
|
68
|
+
const file = sourceFile;
|
|
40
69
|
const checker = program.getTypeChecker();
|
|
41
|
-
const byRange = indexByRange(
|
|
70
|
+
const byRange = indexByRange(file);
|
|
42
71
|
const toTSNode = (node) => {
|
|
43
72
|
const [start, end] = node.range;
|
|
44
|
-
return byRange.get(key(start, end)) ?? deepestContaining(
|
|
73
|
+
return byRange.get(key(start, end)) ?? deepestContaining(file, start, end);
|
|
45
74
|
};
|
|
46
75
|
return {
|
|
47
76
|
program,
|
|
@@ -60,7 +89,14 @@ var FALLBACK_OPTIONS = {
|
|
|
60
89
|
skipLibCheck: true
|
|
61
90
|
};
|
|
62
91
|
var norm = (p) => p.replace(/\\/g, "/");
|
|
63
|
-
var
|
|
92
|
+
var parsedConfigCache = /* @__PURE__ */ new Map();
|
|
93
|
+
var parseConfig = (configPath) => {
|
|
94
|
+
if (parsedConfigCache.has(configPath))
|
|
95
|
+
return parsedConfigCache.get(configPath);
|
|
96
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, ts.sys) ?? void 0;
|
|
97
|
+
parsedConfigCache.set(configPath, parsed);
|
|
98
|
+
return parsed;
|
|
99
|
+
};
|
|
64
100
|
function projectContaining(configPath, filename, seen = /* @__PURE__ */ new Set()) {
|
|
65
101
|
if (seen.has(norm(configPath))) return void 0;
|
|
66
102
|
seen.add(norm(configPath));
|
|
@@ -93,6 +129,48 @@ function getProgram(filename) {
|
|
|
93
129
|
programCache.set(cacheKey, program);
|
|
94
130
|
return program;
|
|
95
131
|
}
|
|
132
|
+
function getStandaloneProgram(filename) {
|
|
133
|
+
let program = standaloneCache.get(filename);
|
|
134
|
+
if (!program) {
|
|
135
|
+
program = ts.createProgram({
|
|
136
|
+
rootNames: [filename],
|
|
137
|
+
options: FALLBACK_OPTIONS
|
|
138
|
+
});
|
|
139
|
+
standaloneCache.set(filename, program);
|
|
140
|
+
}
|
|
141
|
+
return program;
|
|
142
|
+
}
|
|
143
|
+
var inertServices;
|
|
144
|
+
function getInertServices() {
|
|
145
|
+
if (inertServices) return inertServices;
|
|
146
|
+
const name = "__oxlint-plugin-effector-empty__.ts";
|
|
147
|
+
const sf = ts.createSourceFile(name, "", ts.ScriptTarget.Latest, true);
|
|
148
|
+
const host = {
|
|
149
|
+
getSourceFile: (f) => norm(f) === norm(name) ? sf : void 0,
|
|
150
|
+
getDefaultLibFileName: () => "lib.d.ts",
|
|
151
|
+
writeFile: () => void 0,
|
|
152
|
+
getCurrentDirectory: () => "",
|
|
153
|
+
getDirectories: () => [],
|
|
154
|
+
getCanonicalFileName: (f) => f,
|
|
155
|
+
useCaseSensitiveFileNames: () => true,
|
|
156
|
+
getNewLine: () => "\n",
|
|
157
|
+
fileExists: (f) => norm(f) === norm(name),
|
|
158
|
+
readFile: () => ""
|
|
159
|
+
};
|
|
160
|
+
const program = ts.createProgram(
|
|
161
|
+
[name],
|
|
162
|
+
{ noLib: true, skipLibCheck: true },
|
|
163
|
+
host
|
|
164
|
+
);
|
|
165
|
+
inertServices = {
|
|
166
|
+
program,
|
|
167
|
+
esTreeNodeToTSNodeMap: { get: () => void 0, has: () => false },
|
|
168
|
+
tsNodeToESTreeNodeMap: { get: () => void 0, has: () => false },
|
|
169
|
+
getTypeAtLocation: () => void 0,
|
|
170
|
+
getSymbolAtLocation: () => void 0
|
|
171
|
+
};
|
|
172
|
+
return inertServices;
|
|
173
|
+
}
|
|
96
174
|
function deepestContaining(sourceFile, start, end) {
|
|
97
175
|
let best = sourceFile;
|
|
98
176
|
const visit = (node) => {
|
|
@@ -156,7 +234,7 @@ var symbolMatches = (symbol, names, from) => {
|
|
|
156
234
|
return declarations.map((decl) => decl.getSourceFile().fileName).some((fname) => fname.includes("node_modules") && fname.includes(from));
|
|
157
235
|
};
|
|
158
236
|
var typeMatches = (type, names, from, depth = 0) => {
|
|
159
|
-
if (depth > 10) return false;
|
|
237
|
+
if (!type || depth > 10) return false;
|
|
160
238
|
const symbol = type.getSymbol() ?? type.aliasSymbol;
|
|
161
239
|
if (symbol && symbolMatches(symbol, names, from)) return true;
|
|
162
240
|
if (type.isUnion() || type.isIntersection())
|
|
@@ -218,6 +296,7 @@ var enforce_effect_naming_convention_default = createRule({
|
|
|
218
296
|
].join(", ");
|
|
219
297
|
return {
|
|
220
298
|
[identifierSelectors]: (node) => {
|
|
299
|
+
if (bindingCannotBeUnit(node)) return;
|
|
221
300
|
const type = services.getTypeAtLocation(node);
|
|
222
301
|
const isEffect = isType.effect(type);
|
|
223
302
|
if (!isEffect) return;
|
|
@@ -413,6 +492,7 @@ var enforce_gate_naming_convention_default = createRule({
|
|
|
413
492
|
const services = getParserServices(context);
|
|
414
493
|
return {
|
|
415
494
|
[`VariableDeclarator[id.name=${GateRegex}]`]: (node) => {
|
|
495
|
+
if (bindingCannotBeUnit(node.id)) return;
|
|
416
496
|
const type = services.getTypeAtLocation(node);
|
|
417
497
|
const isGate = isType.gate(type);
|
|
418
498
|
if (!isGate) return;
|
|
@@ -479,6 +559,7 @@ var enforce_store_naming_convention_default = createRule({
|
|
|
479
559
|
].join(", ");
|
|
480
560
|
return {
|
|
481
561
|
[identifierSelectors]: (node) => {
|
|
562
|
+
if (bindingCannotBeUnit(node)) return;
|
|
482
563
|
const type = services.getTypeAtLocation(node);
|
|
483
564
|
const isStore = isType.store(type);
|
|
484
565
|
if (!isStore) return;
|
|
@@ -682,6 +763,7 @@ var mandatory_scope_binding_default = createRule({
|
|
|
682
763
|
const inHook = [];
|
|
683
764
|
const isExpectingUnit = (slot) => {
|
|
684
765
|
const tsnode = services.esTreeNodeToTSNodeMap.get(slot);
|
|
766
|
+
if (!tsnode) return false;
|
|
685
767
|
const type = checker.getContextualType(tsnode);
|
|
686
768
|
if (type) return isType.event(type) || isType.effect(type);
|
|
687
769
|
else return false;
|
|
@@ -708,6 +790,7 @@ var mandatory_scope_binding_default = createRule({
|
|
|
708
790
|
const name = nameOf.function(node);
|
|
709
791
|
if (name && UseRegex.test(name.name)) return void inRender.push(true);
|
|
710
792
|
const tsnode = services.esTreeNodeToTSNodeMap.get(node);
|
|
793
|
+
if (!tsnode) return void inRender.push(false);
|
|
711
794
|
const signature = checker.getSignatureFromDeclaration(tsnode);
|
|
712
795
|
const returnType = signature ? checker.getReturnTypeOfSignature(signature) : void 0;
|
|
713
796
|
const isJSX = matchesType(returnType, (type) => isType.jsx(type));
|
|
@@ -1436,6 +1519,7 @@ var no_units_spawn_in_render_default = createRule({
|
|
|
1436
1519
|
if (name && UseRegex2.test(name.name))
|
|
1437
1520
|
return void stack.render.push(true);
|
|
1438
1521
|
const tsnode = services.esTreeNodeToTSNodeMap.get(node);
|
|
1522
|
+
if (!tsnode) return void stack.render.push(false);
|
|
1439
1523
|
const signature = checker.getSignatureFromDeclaration(tsnode);
|
|
1440
1524
|
const returnType = signature ? checker.getReturnTypeOfSignature(signature) : void 0;
|
|
1441
1525
|
const isJSX = matchesType2(returnType, (type) => isType.jsx(type));
|
|
@@ -1559,6 +1643,7 @@ function getCalleeName(callee) {
|
|
|
1559
1643
|
else return null;
|
|
1560
1644
|
}
|
|
1561
1645
|
function hasEffectorUnitInType(ctx, type, depth = 3) {
|
|
1646
|
+
if (!type) return false;
|
|
1562
1647
|
if (isType.unit(type)) return true;
|
|
1563
1648
|
if (depth <= 0) return false;
|
|
1564
1649
|
if (type.isUnion())
|
package/package.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-effector",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Enforcing best practices for Effector — a full type-aware oxlint plugin (port of eslint-plugin-effector)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Marsel Abazbekov <marsel.ave@gmail.com>",
|
|
5
7
|
"keywords": [
|
|
6
8
|
"oxlint",
|
|
7
9
|
"oxlint-plugin",
|