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.
Files changed (3) hide show
  1. package/README.md +20 -6
  2. package/dist/index.js +96 -11
  3. 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. Under ESLint / the test runner it transparently uses the parser's existing
16
- services; under oxlint it constructs its own. Same rule code, real types, either way. There is **no
17
- `@typescript-eslint` in the runtime** — the only runtime dependencies are `typescript` (peer) and `esquery`.
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
- Trade-off: building a TypeScript program means these rules are not "native oxlint" fast — the first lint of a
27
- project pays for program creation. Functionally there are **no limitations** versus `eslint-plugin-effector`
28
- (the full upstream test suite 349 cases passes against the real type checker).
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
- return buildServices(context);
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
- const program = getProgram(filename);
35
- const sourceFile = program.getSourceFile(filename);
36
- if (!sourceFile)
37
- throw new Error(
38
- `oxlint-plugin-effector: cannot load "${filename}" into a TypeScript program`
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(sourceFile);
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(sourceFile, start, end);
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 parseConfig = (configPath) => ts.getParsedCommandLineOfConfigFile(configPath, {}, ts.sys) ?? void 0;
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.1",
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",