@geonosis/oxlint-plugin-biological-architecture 3.2.0 → 3.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +27 -1
  3. package/RULES.md +7 -7
  4. package/corpus/medusa/queries/bad-silent-cap.ts +11 -0
  5. package/corpus/medusa/queries/good-paged-through.ts +19 -0
  6. package/corpus/medusa/queries/integration-tests/any-row.ts +10 -0
  7. package/dist/index.js +3 -1
  8. package/dist/presets.js +7 -2
  9. package/dist/rules/lib/direction-rule.d.ts +1 -0
  10. package/dist/rules/lib/direction-rule.js +13 -1
  11. package/dist/rules/lib/doors.js +4 -2
  12. package/dist/rules/lib/fresh.d.ts +5 -0
  13. package/dist/rules/lib/fresh.js +15 -0
  14. package/dist/rules/lib/options.d.ts +1 -0
  15. package/dist/rules/lib/options.js +5 -0
  16. package/dist/rules/lib/run-files.d.ts +18 -0
  17. package/dist/rules/lib/run-files.js +144 -0
  18. package/dist/rules/lib/token-layer.d.ts +19 -0
  19. package/dist/rules/lib/token-layer.js +170 -0
  20. package/dist/rules/lib/workspace-members.js +17 -19
  21. package/dist/rules/list-take-without-order.d.ts +24 -0
  22. package/dist/rules/list-take-without-order.js +293 -10
  23. package/dist/rules/no-orm-outside-db.js +1 -0
  24. package/dist/rules/subpath-over-barrel.d.ts +1 -0
  25. package/dist/rules/subpath-over-barrel.js +18 -12
  26. package/dist/rules/test-no-clock-polling-for-promise-flips.d.ts +5 -1
  27. package/dist/rules/test-no-clock-polling-for-promise-flips.js +3 -0
  28. package/dist/rules/tissue-must-compose.d.ts +1 -1
  29. package/dist/rules/tissue-no-data-props.d.ts +2 -2
  30. package/dist/rules/tokens-only-colors.d.ts +8 -11
  31. package/dist/rules/tokens-only-colors.js +36 -7
  32. package/dist/rules/worker-handles-are-scoped.d.ts +1 -1
  33. package/dist/timing.d.ts +18 -0
  34. package/dist/timing.js +75 -0
  35. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @geonosis/oxlint-plugin-biological-architecture
2
2
 
3
+ ## 3.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 17dc880: `list-take-without-order` reads the D1 stack's three spellings of a page read in no order, and joins the `backend-d1` preset (#72): a Drizzle select chain with `.limit()` and no `.orderBy()`; a relational `findMany({ limit })` with no `orderBy`, or a `findFirst` — `LIMIT 1` — with no `orderBy`; and on the raw binding a `prepare()` SELECT with LIMIT and no ORDER BY, or a `.first()` over a SELECT with no ORDER BY (D1's `.first()` does not alter the statement). A `where` that is `eq(t.<unique>, …)`, or a SQL WHERE `<unique> = ?` with no OR, pins the row; the columns are the new `uniqueColumns` option (default `id`). An aggregate SELECT with no GROUP BY is one row. The rule now presumes no single engine. A `backend-d1` repo sees new findings where these spellings occur.
8
+
9
+ ### Patch Changes
10
+
11
+ - fb08b28: A refusal that stands in for every file a rule did not read (a rule enabled without an option, a root import with no package resolving) is carried by the lexically first file the rule judges, not the first file oxlint happens to visit, so two runs of one tree print the same lines. The file is read off the command line, a JSON config and `git ls-files`; a run the plugin cannot read behaves as before. `unresolvedPackage` is now said once in a file, not once in a run. The varying counts reported for `ssot-no-inline-facts`, `constants-in-constants-file`, `max-comment-density` and `ssot-no-process-env` were not those rules: oxlint 1.87 on macOS drops lines of its stdout when it is a shell pipe, and a plugin with one rule that reports every string shows it. The README says so.
12
+ - fb08b28: `GEONOSIS_LINT_TIMING=1` makes the plugin time each rule's visitor set-up and handlers and print the slowest ten to stderr when the run ends; `GEONOSIS_LINT_TIMING_TOP` lists more and `GEONOSIS_LINT_TIMING_FILE` writes every rule as JSON (#53). Without the variable nothing is measured. The timing found where one lint of this repo spent its time: the workspace and `node_modules` lookups read manifests on every import, every `directionRule` recomputed an import's landings, `no-orm-outside-db` computed them for imports of anything, and `test-no-clock-polling-for-promise-flips` walked every function of a test that names no `waitFor`. Lookups are now kept for one second, an import's landings are computed once for all direction rules, and the two rules skip what they cannot report; no rule's findings change, and the whole lint of this repo takes 29% less CPU (7.2 s to 5.1 s on average over five runs each).
13
+ - fb08b28: `list-take-without-order` no longer judges test files by default (`__tests__/`, `.test.`, `.spec.`, `integration-tests/`): the row a fixture takes does not matter, and 72 of one tree's 198 findings were fixtures. `exempt` replaces that list and `exempt: []` judges test files again; a baseline keyed on the old count falls by the test findings. A literal `take` above 1 from the first row, whose result is iterated, counted or handed whole to another call, now gets the message `truncated` ("reads at most N rows and the code treats them as all of them") instead of "name the column", which left the dropped rows dropped. Since 3.2.1, a capped read used as if complete is reported even when it names an order, because ordering does not make the cap all the rows; the count may rise. Same rule id throughout, so no consumer's counter changes by key.
14
+ - fb08b28: `tokens-only-colors`: since 3.2.0 the rule also reads colour literals in plain object properties, which is why a count rose without a change in the code (one Medusa storefront went from 3 to 69 findings; the 3.2.0 entry named only Tailwind variants). A theme passed to `defineTheme` (or to `compile`, `toDtcg`, `emitVariables`, `valueAt`) from `@geonosis/themekit` is the token layer and is no longer judged, whether the object is written in the call or held in a `const` of the same file. So is an object literal typed as a themekit palette type (`Theme`, `ThemeColors`, `ThemeMode` and the other types a `Theme` is made of) imported from `@geonosis/themekit`, by a `const` annotation, `satisfies` or `as`, so a brand theme defined in its own module is covered too; a local type of the same name is not. Their `exemptModules` entries can go. The new option `ignoreKeys` names property keys whose values are data that only look like colours (a shader's mask channels); it is empty by default and changes nothing until set.
15
+
3
16
  ## 3.2.0
4
17
 
5
18
  ### Minor Changes
package/README.md CHANGED
@@ -63,7 +63,7 @@ such as an open flag is allowed there. A cell never composes another cell.
63
63
  | `backend-medusa` | 13 | Medusa steps, emits, subscribers, loaders and the store route surface |
64
64
  | `tenant-scoping` | 3 | every row placed, scoped and narrowed to one tenant |
65
65
  | `tooling` | 3 | a repo's own layers as data, no generic `utils` package, debt markers name a plan |
66
- | `backend-d1` | 2 | Cloudflare D1 facts — false on an engine with transactions |
66
+ | `backend-d1` | 3 | Cloudflare D1: no transactions, scoped handles, and a page read in no order (Drizzle and the raw binding) |
67
67
  | `themekit` | 2 | colours and fonts only through the theme's tokens and roles |
68
68
  | `testbed` | 3 | integration suites run on a real database; tests await rather than poll |
69
69
  | `backend-trpc` | 1 | every router has its schema, and every schema its router |
@@ -242,6 +242,32 @@ Then find the stray copy, and mind that the answer is package-manager-specific:
242
242
  `.pnpm`. Read the links instead: `ls -l node_modules/@geonosis` in each workspace, and check the
243
243
  version in the target path.
244
244
 
245
+ ## Where the plugin spends its time
246
+
247
+ `GEONOSIS_LINT_TIMING=1 oxlint …` prints, to stderr when the run ends, the rules that cost the most:
248
+ the time each spent building its visitor (`create`, once per file) and inside its handlers
249
+ (`visit`), and the files it was handed. `GEONOSIS_LINT_TIMING_TOP=25` lists more than the ten
250
+ slowest, and `GEONOSIS_LINT_TIMING_FILE=timing.json` writes every rule as JSON. Without the variable
251
+ nothing is measured and nothing is printed. A rule that pays for a shared index (the workspace and
252
+ `node_modules` lookups, the export tables) pays on its first use, so read the numbers as where a run
253
+ spent its time and not as what one rule costs on its own. The lookups are kept for one second, so a
254
+ host that edits a `package.json` sees the edit within it.
255
+
256
+ ## A run reads the same every time
257
+
258
+ Every rule's findings are a function of the tree alone. The one thing that was not is where a
259
+ refusal lands: a rule enabled without an option reports once a run, "standing in for every file it did
260
+ not read", and oxlint visits files in an order that changes between runs. When the plugin can read the
261
+ run (oxlint started from the command line, a JSON config, a git checkout) the refusal is carried by the
262
+ lexically first file the rule judges: files git ignores or `ignorePatterns` names are not candidates,
263
+ nor is a file that switches the lint off, nor one an override for that rule matches. In any other run it
264
+ lands on the first file visited, as before.
265
+
266
+ oxlint 1.87 on macOS loses lines of its own output when its stdout is a shell pipe and the output passes
267
+ the pipe's buffer (`oxlint … | sort`, 6,000 findings: 5,445 to 6,294 lines over six runs; a plugin with one
268
+ rule that reports every string reproduces it). Written to a file, or read by a program that spawns it,
269
+ the same run gives the same lines every time. Count findings from the file, not from a pipeline.
270
+
245
271
  ## Where to read on
246
272
 
247
273
  - `RULES.md` — in this tarball: every rule, its preset, which source tree it came from, how each
package/RULES.md CHANGED
@@ -11,12 +11,12 @@ One plugin name, two rule sets, no shared code. **33 rules had the same filename
11
11
 
12
12
  The union here is **52 rules** — 54 when it was cut, less `document-sagas-are-generic` and
13
13
  `documents-share-one-table`, retired in 2.7.0 because both enforced one consumer's document kinds —
14
- plus thirty the kit wrote for itself: `layer-walls`, `emit-declares-attempts`,
14
+ plus thirty-one the kit wrote for itself: `layer-walls`, `emit-declares-attempts`,
15
15
  `cell-no-cross-feature-organelles`, the `backend-medusa` ten, `no-unregistered-plugin-dir` +
16
16
  `router-schema-parity`, the `themekit` two, `no-mock-db-in-integration`, `todo-names-its-plan`,
17
17
  `boundary-time-is-primitive`, `durable-body-reads-through-steps`, `subpath-over-barrel` and the six
18
- of the W67 batch, `icon-control-has-name` and `tissue-no-fetch` — **82**. The two retired ids went in 3.0.0.
19
- Every one of the thirty is marked **written here** in the provenance table, and the counts below
18
+ of the W67 batch, `icon-control-has-name`, `tissue-no-fetch` and `hermetic-e2e-no-wall-clock-waits` — **83**. The two retired ids went in 3.0.0.
19
+ Every one of the thirty-one is marked **written here** in the provenance table, and the counts below
20
20
  are generated from that column and from the rules the plugin exports.
21
21
 
22
22
  <!-- GENERATED: the rules by where they came from -->
@@ -732,7 +732,7 @@ every repo's.
732
732
  | `hermetic-e2e-no-wall-clock-waits` | testbed | — | — |
733
733
  | `icon-control-has-name` | frontend | — | — |
734
734
  | `layer-walls` | tooling | `layers` | — |
735
- | `list-take-without-order` | backend-medusa | — | Medusa |
735
+ | `list-take-without-order` | backend-d1 + backend-medusa | — | — |
736
736
  | `max-comment-density` | comment-ceiling | — | — |
737
737
  | `molecule-atoms-only` | frontend | — | — |
738
738
  | `molecule-must-compose` | frontend | — | — |
@@ -798,7 +798,7 @@ every repo's.
798
798
  | `worker-handles-are-scoped` | backend-d1 | `within` | — |
799
799
  | `zustand-v5-best-practices` | frontend | — | — |
800
800
 
801
- Preset sizes: frontend 39 · backend-workflows 15 · backend-medusa 13 · tenant-scoping 3 · testbed 3 · tooling 3 · backend-d1 2 · themekit 2 · backend-workflows-cf 1 · backend-trpc 1 · integrations 1 · package-subpaths 1 · comment-ceiling 1 · failure-paths 1.
801
+ Preset sizes: frontend 39 · backend-workflows 15 · backend-medusa 13 · backend-d1 3 · tenant-scoping 3 · testbed 3 · tooling 3 · themekit 2 · backend-workflows-cf 1 · backend-trpc 1 · integrations 1 · package-subpaths 1 · comment-ceiling 1 · failure-paths 1.
802
802
 
803
803
  <!-- END GENERATED INVENTORY -->
804
804
 
@@ -981,7 +981,7 @@ option not named here is one no rule reads.
981
981
  | `hermetic-e2e-no-wall-clock-waits` | `clockWaits`, `listEndpoints`, `networkWaits`, `specs` |
982
982
  | `icon-control-has-name` | `controls`, `icons` |
983
983
  | `layer-walls` | `externals`, `layers`, `typeOnly` |
984
- | `list-take-without-order` | `callees`, `orderKeys` |
984
+ | `list-take-without-order` | `callees`, `exempt`, `orderKeys`, `uniqueColumns` |
985
985
  | `max-comment-density` | — |
986
986
  | `molecule-atoms-only` | — |
987
987
  | `molecule-must-compose` | — |
@@ -1042,7 +1042,7 @@ option not named here is one no rule reads.
1042
1042
  | `tissue-no-organelles` | — |
1043
1043
  | `tissue-no-stores` | — |
1044
1044
  | `todo-names-its-plan` | `id`, `markers` |
1045
- | `tokens-only-colors` | `allow`, `exemptModules`, `tokenPrefix` |
1045
+ | `tokens-only-colors` | `allow`, `exemptModules`, `ignoreKeys`, `tokenPrefix` |
1046
1046
  | `unique-query-step-name` | `queryStepCalls`, `workflowFactories` |
1047
1047
  | `worker-handles-are-scoped` | `within`, `wrapper` |
1048
1048
  | `zustand-v5-best-practices` | — |
@@ -0,0 +1,11 @@
1
+ // BREACH: the cap is not a page. The 101st production order is never cancelled, in any order.
2
+ declare const service: {
3
+ listProductionOrders: (filters: unknown, config: unknown) => Promise<{ id: string }[]>
4
+ }
5
+ declare function cancelOrders(ids: string[]): Promise<void>
6
+
7
+ export const cancelAll = async (orderId: string): Promise<void> => {
8
+ const candidates = await service.listProductionOrders({ order_id: orderId }, { take: 100 })
9
+
10
+ await cancelOrders(candidates.map((one) => one.id))
11
+ }
@@ -0,0 +1,19 @@
1
+ // ACCEPTED: every page names its order and the loop reads pages until one comes back short.
2
+ declare const service: {
3
+ listProductionOrders: (filters: unknown, config: unknown) => Promise<{ id: string }[]>
4
+ }
5
+ declare function cancelOrders(ids: string[]): Promise<void>
6
+
7
+ const PAGE = 100
8
+
9
+ export const cancelAll = async (orderId: string): Promise<void> => {
10
+ for (let skip = 0; ; skip += PAGE) {
11
+ const page = await service.listProductionOrders(
12
+ { order_id: orderId },
13
+ { order: { id: 'ASC' }, skip, take: PAGE },
14
+ )
15
+
16
+ await cancelOrders(page.map((one) => one.id))
17
+ if (page.length < PAGE) return
18
+ }
19
+ }
@@ -0,0 +1,10 @@
1
+ // ACCEPTED: a fixture that wants any row; which row comes back does not matter.
2
+ declare const service: {
3
+ listSalesChannels: (filters: unknown, config: unknown) => Promise<{ id: string }[]>
4
+ }
5
+
6
+ export const anyChannel = async (): Promise<unknown> => {
7
+ const [channel] = await service.listSalesChannels({}, { take: 1 })
8
+
9
+ return channel
10
+ }
package/dist/index.js CHANGED
@@ -4,9 +4,11 @@ import { PRESETS, PRESUMED_ENGINES, TIER_RULES } from "./presets.js";
4
4
  import { registered } from "./registry.js";
5
5
  import { reportingOncePerRun } from "./rules/lib/options.js";
6
6
  import { TIERS } from "./rules/lib/tiers.js";
7
+ import { timingRequested, withTiming } from "./timing.js";
7
8
  import { PRESETS as PRESETS2, presetSections, RENAMED_TO, rulesOfPreset } from "./presets.js";
8
9
  var presumed = new Map(PRESUMED_ENGINES.flatMap((one) => one.rules.map((name) => [name, { engine: one.engine, packages: one.packages }])));
9
10
  var reached = new Map(TIERS.flatMap((tier) => TIER_RULES[tier].map((name) => [name, tier])));
11
+ var timed = timingRequested();
10
12
  var rules = Object.fromEntries(Object.entries(registered).map(([name, rule]) => {
11
13
  const presumes = presumed.get(name);
12
14
  const tier = reached.get(name);
@@ -16,7 +18,7 @@ var rules = Object.fromEntries(Object.entries(registered).map(([name, rule]) =>
16
18
  ...rule,
17
19
  ...presumes === undefined ? {} : { presumes },
18
20
  ...tier === undefined ? {} : { tier },
19
- create: reportingOncePerRun(rule.create, name)
21
+ create: timed ? withTiming(reportingOncePerRun(rule.create, name), name) : reportingOncePerRun(rule.create, name)
20
22
  }
21
23
  ];
22
24
  }));
package/dist/presets.js CHANGED
@@ -57,7 +57,11 @@ var BACKEND_WORKFLOWS = [
57
57
  "tables-declare-their-plane",
58
58
  "time-through-the-door"
59
59
  ];
60
- var BACKEND_D1 = ["no-d1-transaction", "worker-handles-are-scoped"];
60
+ var BACKEND_D1 = [
61
+ "list-take-without-order",
62
+ "no-d1-transaction",
63
+ "worker-handles-are-scoped"
64
+ ];
61
65
  var BACKEND_WORKFLOWS_CF = ["step-opens-its-own-cell"];
62
66
  var RENAMED_TO = {
63
67
  "backend-sagaflow-cf": "backend-workflows-cf"
@@ -110,11 +114,12 @@ var COMMENT_CEILING = {
110
114
  "capitalized-comments": "error"
111
115
  }
112
116
  };
117
+ var ACROSS_ENGINES = new Set(["list-take-without-order"]);
113
118
  var PRESUMED_ENGINES = [
114
119
  {
115
120
  engine: "Medusa",
116
121
  packages: ["@medusajs/framework", "@medusajs/medusa", "@medusajs/utils"],
117
- rules: BACKEND_MEDUSA.filter((name) => !WORKFLOW_RULES.has(name))
122
+ rules: BACKEND_MEDUSA.filter((name) => !WORKFLOW_RULES.has(name) && !ACROSS_ENGINES.has(name))
118
123
  }
119
124
  ];
120
125
  var TIER_RULES = {
@@ -14,6 +14,7 @@ type Declaration<Options> = {
14
14
  description: string;
15
15
  fixShape: string;
16
16
  forbidden: (target: Classified, importer: Classified, options: Options) => boolean;
17
+ mayForbid?: (source: string) => boolean;
17
18
  messageId?: string;
18
19
  probe?: (options: readonly unknown[]) => Probe | ProbeTree;
19
20
  messages: Record<string, string>;
@@ -30,7 +30,7 @@ var namespaceBoundBy = (node) => {
30
30
  return typeof local === "string" ? local : undefined;
31
31
  };
32
32
  var WHOLE = "whole";
33
- var targetsOf = (filename, importer, source, taken) => {
33
+ var targetsFor = (filename, importer, source, taken) => {
34
34
  const spelled = classify(resolveRelative(importer.path, source), source);
35
35
  const door = landingsOf(filename, source, []).door;
36
36
  const landed = landingsOf(filename, source, taken === WHOLE ? door === undefined ? [] : [...exportedNamesOf(door)] : taken);
@@ -48,6 +48,16 @@ var targetsOf = (filename, importer, source, taken) => {
48
48
  targets.push(classify(judged(landed.door), source));
49
49
  return targets;
50
50
  };
51
+ var targetsMemo = new Map;
52
+ var targetsOf = (filename, importer, source, taken) => {
53
+ const key = `${filename}\x00${source}\x00${taken === WHOLE ? WHOLE : taken.join("\x00")}`;
54
+ const known = targetsMemo.get(key);
55
+ if (known !== undefined)
56
+ return known;
57
+ const found = targetsFor(filename, importer, source, taken);
58
+ targetsMemo.set(key, found);
59
+ return found;
60
+ };
51
61
  var directionRule = (declaration) => {
52
62
  const {
53
63
  data = placeAndSource,
@@ -64,6 +74,8 @@ var directionRule = (declaration) => {
64
74
  if (!inScope(importer))
65
75
  return {};
66
76
  const judge = (source, node, taken) => {
77
+ if (declaration.mayForbid?.(source) === false)
78
+ return;
67
79
  const target = targetsOf(context.filename, importer, source, taken).find((one) => declaration.forbidden(one, importer, options));
68
80
  if (target === undefined)
69
81
  return;
@@ -1,6 +1,7 @@
1
1
  // src/rules/lib/doors.ts
2
2
  import { existsSync, lstatSync, readdirSync, realpathSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
+ import { remembered } from "./fresh.js";
4
5
  import { exportedNamesOf } from "./landing.js";
5
6
  import { conditionTargetOf, fileFor, readManifest, recordOf } from "./module-text.js";
6
7
  import { workspaceMemberNamed } from "./workspace-members.js";
@@ -78,7 +79,8 @@ var doorsOf = (manifestPath) => {
78
79
  }
79
80
  return { homes: placed, name: manifest.name };
80
81
  };
81
- var resolveInNodeModules = (from, name) => {
82
+ var resolvedIn = new Map;
83
+ var resolveInNodeModules = (from, name) => remembered(resolvedIn, `${dirname(from)}\x00${name}`, () => {
82
84
  let dir = dirname(from);
83
85
  for (;; ) {
84
86
  const packageDir = join(dir, "node_modules", name);
@@ -99,7 +101,7 @@ var resolveInNodeModules = (from, name) => {
99
101
  return;
100
102
  dir = parent;
101
103
  }
102
- };
104
+ });
103
105
  var resolvePackage = (from, name) => {
104
106
  const found = resolveInNodeModules(from, name);
105
107
  if (found?.own === true)
@@ -0,0 +1,5 @@
1
+ export declare const FRESH_MS = 1000;
2
+ export declare const remembered: <Value>(held: Map<string, {
3
+ at: number;
4
+ value: Value;
5
+ }>, key: string, compute: () => Value) => Value;
@@ -0,0 +1,15 @@
1
+ // src/rules/lib/fresh.ts
2
+ var FRESH_MS = 1000;
3
+ var remembered = (held, key, compute) => {
4
+ const now = Date.now();
5
+ const known = held.get(key);
6
+ if (known !== undefined && now - known.at < FRESH_MS)
7
+ return known.value;
8
+ const value = compute();
9
+ held.set(key, { at: now, value });
10
+ return value;
11
+ };
12
+ export {
13
+ FRESH_MS,
14
+ remembered
15
+ };
@@ -20,5 +20,6 @@ export declare const DEFAULT_TENANT_KEY = "orgId";
20
20
  export declare const DEFAULT_TEST_EXEMPT: string[];
21
21
  export declare const requireOption: <Value>(rule: string, value: Value | null | undefined, option: string, accepts?: string) => Value;
22
22
  type Create<Context> = (context: Context) => Record<string, unknown>;
23
+ export declare const onceFor: (key: string, file: string) => boolean;
23
24
  export declare const reportingOncePerRun: <Context>(create: Create<Context>, rule?: string) => Create<Context>;
24
25
  export {};
@@ -1,4 +1,5 @@
1
1
  // src/rules/lib/options.ts
2
+ import { designatedFile } from "./run-files.js";
2
3
  var isMissing = (value) => value === undefined || value === null || value === "" || Array.isArray(value) && value.length === 0;
3
4
  var refusalHeader = (rule, option) => `biological-architecture/${rule} requires option \`${option}\`.`;
4
5
 
@@ -36,6 +37,9 @@ var badPatternMessage = (rule, error) => `biological-architecture/${rule} threw
36
37
  var onceFor = (key, file) => {
37
38
  if (file === "")
38
39
  return true;
40
+ const designated = designatedFile(key.split("/")[0] ?? key);
41
+ if (designated !== null)
42
+ return file === designated;
39
43
  const first = raisedOver.get(key);
40
44
  if (first !== undefined && first !== file)
41
45
  return false;
@@ -83,6 +87,7 @@ export {
83
87
  DEFAULT_TEST_EXEMPT,
84
88
  OptionMissing,
85
89
  PATTERNS,
90
+ onceFor,
86
91
  patternsOf,
87
92
  refusalHeader,
88
93
  reportingOncePerRun,
@@ -0,0 +1,18 @@
1
+ type Override = {
2
+ files: string[];
3
+ rules: Record<string, unknown>;
4
+ };
5
+ type Run = {
6
+ candidates: string[];
7
+ overrides: Override[];
8
+ prefixes: string[];
9
+ root: string;
10
+ rules: Record<string, unknown>;
11
+ };
12
+ export declare const runOf: ({ argv, cwd }: {
13
+ argv: readonly string[];
14
+ cwd: string;
15
+ }) => Run | null;
16
+ export declare const designatedIn: (run: Run, rule: string, accepts: (file: string, text: string) => boolean) => string | null;
17
+ export declare const designatedFile: (rule: string, salt?: string, accepts?: (file: string, text: string) => boolean) => string | null;
18
+ export {};
@@ -0,0 +1,144 @@
1
+ // src/rules/lib/run-files.ts
2
+ import { spawnSync } from "node:child_process";
3
+ import { closeSync, existsSync, openSync, readFileSync, readSync } from "node:fs";
4
+ import { basename, dirname, join, matchesGlob, relative, resolve } from "node:path";
5
+ import { parseLooseJson } from "./module-text.js";
6
+ var LINTED = /\.(?:[cm]?[jt]sx?)$/;
7
+ var HEAD_BYTES = 4096;
8
+ var SWITCHES_OFF = /\b(?:oxlint|eslint)-disable(?!-(?:next-)?line)\b/;
9
+ var PLUGIN_NAME = "biological-architecture";
10
+ var FLAGS_WITH_VALUE = new Set([
11
+ "-c",
12
+ "--config",
13
+ "-f",
14
+ "--format",
15
+ "--threads",
16
+ "--tsconfig",
17
+ "-D",
18
+ "-W",
19
+ "-A",
20
+ "--deny",
21
+ "--warn",
22
+ "--allow",
23
+ "--max-warnings"
24
+ ]);
25
+ var UNKNOWABLE = ["--ignore-path", "--ignore-pattern", "--no-ignore", "--lsp", "--stdin-filepath"];
26
+ var recordOf = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : {};
27
+ var stringsOf = (value) => Array.isArray(value) ? value.filter((one) => typeof one === "string") : [];
28
+ var ignoredBy = (path, patterns) => patterns.some((pattern) => {
29
+ const bare = pattern.replace(/^\.\//, "").replace(/\/$/, "");
30
+ if (!bare.includes("/"))
31
+ return path.split("/").some((segment) => matchesGlob(segment, bare));
32
+ return matchesGlob(path, bare) || matchesGlob(path, `${bare}/**`);
33
+ });
34
+ var looselyMatches = (path, glob) => matchesGlob(path, glob) || matchesGlob(path, `**/${glob}`) || matchesGlob(basename(path), glob);
35
+ var switchedOff = (file) => {
36
+ let handle;
37
+ try {
38
+ handle = openSync(file, "r");
39
+ const buffer = Buffer.alloc(HEAD_BYTES);
40
+ const read = readSync(handle, buffer, 0, HEAD_BYTES, 0);
41
+ return SWITCHES_OFF.test(buffer.subarray(0, read).toString("utf8"));
42
+ } catch {
43
+ return true;
44
+ } finally {
45
+ if (handle !== undefined)
46
+ closeSync(handle);
47
+ }
48
+ };
49
+ var prefixesOf = (jsPlugins) => {
50
+ const names = (Array.isArray(jsPlugins) ? jsPlugins : []).flatMap((entry) => {
51
+ if (typeof entry === "string") {
52
+ return entry.includes("oxlint-plugin-biological-architecture") ? [PLUGIN_NAME] : [];
53
+ }
54
+ const { name, specifier } = recordOf(entry);
55
+ return typeof name === "string" && typeof specifier === "string" && specifier.includes("oxlint-plugin-biological-architecture") ? [name] : [];
56
+ });
57
+ return names.length === 0 ? [PLUGIN_NAME] : names;
58
+ };
59
+ var runOf = ({ argv, cwd }) => {
60
+ let config;
61
+ const paths = [];
62
+ for (let at = 0;at < argv.length; at += 1) {
63
+ const word = argv[at] ?? "";
64
+ const name = word.split("=")[0] ?? word;
65
+ if (UNKNOWABLE.includes(name))
66
+ return null;
67
+ if (name === "-c" || name === "--config") {
68
+ config = word.includes("=") ? word.slice(word.indexOf("=") + 1) : argv[at += 1];
69
+ } else if (FLAGS_WITH_VALUE.has(word))
70
+ at += 1;
71
+ else if (!word.startsWith("-"))
72
+ paths.push(word);
73
+ }
74
+ const configPath = config === undefined ? join(cwd, ".oxlintrc.json") : resolve(cwd, config);
75
+ if (!/\.jsonc?$/.test(configPath) || !existsSync(configPath))
76
+ return null;
77
+ const declared = parseLooseJson(readFileSync(configPath, "utf8"));
78
+ const record = recordOf(declared);
79
+ const patterns = stringsOf(record["ignorePatterns"]);
80
+ if (patterns.some((one) => one.startsWith("!")))
81
+ return null;
82
+ const listed = spawnSync("git", ["ls-files", "-co", "--exclude-standard", "-z", "--", ...paths.length === 0 ? ["."] : paths], { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
83
+ if (listed.status !== 0)
84
+ return null;
85
+ const root = dirname(configPath);
86
+ return {
87
+ candidates: listed.stdout.split("\x00").filter((path) => LINTED.test(path)).map((path) => resolve(cwd, path)).filter((path) => existsSync(path) && !ignoredBy(relative(root, path), patterns)).toSorted(),
88
+ overrides: (Array.isArray(record["overrides"]) ? record["overrides"] : []).map((one) => ({
89
+ files: stringsOf(recordOf(one)["files"]),
90
+ rules: recordOf(recordOf(one)["rules"])
91
+ })),
92
+ prefixes: prefixesOf(record["jsPlugins"]),
93
+ root,
94
+ rules: recordOf(record["rules"])
95
+ };
96
+ };
97
+ var isOff = (level) => {
98
+ const first = Array.isArray(level) ? level[0] : level;
99
+ return first === "off" || first === 0;
100
+ };
101
+ var designatedIn = (run, rule, accepts) => {
102
+ const keys = run.prefixes.map((prefix) => `${prefix}/${rule}`);
103
+ const base = keys.map((key) => run.rules[key]).find((level) => level !== undefined);
104
+ if (isOff(base))
105
+ return null;
106
+ const mentioning = run.overrides.filter((one) => keys.some((key) => (key in one.rules)));
107
+ return run.candidates.find((path) => {
108
+ const here = relative(run.root, path);
109
+ if (mentioning.some((one) => one.files.some((glob) => looselyMatches(here, glob)))) {
110
+ return false;
111
+ }
112
+ if (switchedOff(path))
113
+ return false;
114
+ try {
115
+ return accepts(path, readFileSync(path, "utf8"));
116
+ } catch {
117
+ return false;
118
+ }
119
+ }) ?? null;
120
+ };
121
+ var HOST = /[\\/]oxlint[\\/]bin[\\/]oxlint(?:\.\w+)?$/;
122
+ var run;
123
+ var designated = new Map;
124
+ var designatedFile = (rule, salt = "", accepts = () => true) => {
125
+ if (run === undefined) {
126
+ const [, script = "", ...argv] = process.argv;
127
+ try {
128
+ run = HOST.test(script) ? runOf({ argv, cwd: process.cwd() }) : null;
129
+ } catch {
130
+ run = null;
131
+ }
132
+ }
133
+ if (run === null)
134
+ return null;
135
+ const key = `${rule}\x00${salt}`;
136
+ if (!designated.has(key))
137
+ designated.set(key, designatedIn(run, rule, accepts));
138
+ return designated.get(key) ?? null;
139
+ };
140
+ export {
141
+ designatedFile,
142
+ designatedIn,
143
+ runOf
144
+ };
@@ -0,0 +1,19 @@
1
+ import { type ModuleNode } from './imports.js';
2
+ import type { AnyNode } from './nodes.js';
3
+ export declare const THEMEKIT = "@geonosis/themekit";
4
+ export declare const PALETTE_TAKERS: readonly string[];
5
+ export declare const PALETTE_TYPES: readonly string[];
6
+ export declare const tokenLayerOf: () => {
7
+ covers: (chain: readonly AnyNode[]) => boolean;
8
+ enclosing: () => AnyNode[];
9
+ resolve: () => void;
10
+ visitor: {
11
+ CallExpression(node: AnyNode): void;
12
+ ImportDeclaration(node: ModuleNode): void;
13
+ TSAsExpression(node: AnyNode): void;
14
+ TSSatisfiesExpression(node: AnyNode): void;
15
+ ObjectExpression(node: AnyNode): void;
16
+ 'ObjectExpression:exit'(): void;
17
+ VariableDeclaration(node: AnyNode): void;
18
+ };
19
+ };