@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.
- package/CHANGELOG.md +13 -0
- package/README.md +27 -1
- package/RULES.md +7 -7
- package/corpus/medusa/queries/bad-silent-cap.ts +11 -0
- package/corpus/medusa/queries/good-paged-through.ts +19 -0
- package/corpus/medusa/queries/integration-tests/any-row.ts +10 -0
- package/dist/index.js +3 -1
- package/dist/presets.js +7 -2
- package/dist/rules/lib/direction-rule.d.ts +1 -0
- package/dist/rules/lib/direction-rule.js +13 -1
- package/dist/rules/lib/doors.js +4 -2
- package/dist/rules/lib/fresh.d.ts +5 -0
- package/dist/rules/lib/fresh.js +15 -0
- package/dist/rules/lib/options.d.ts +1 -0
- package/dist/rules/lib/options.js +5 -0
- package/dist/rules/lib/run-files.d.ts +18 -0
- package/dist/rules/lib/run-files.js +144 -0
- package/dist/rules/lib/token-layer.d.ts +19 -0
- package/dist/rules/lib/token-layer.js +170 -0
- package/dist/rules/lib/workspace-members.js +17 -19
- package/dist/rules/list-take-without-order.d.ts +24 -0
- package/dist/rules/list-take-without-order.js +293 -10
- package/dist/rules/no-orm-outside-db.js +1 -0
- package/dist/rules/subpath-over-barrel.d.ts +1 -0
- package/dist/rules/subpath-over-barrel.js +18 -12
- package/dist/rules/test-no-clock-polling-for-promise-flips.d.ts +5 -1
- package/dist/rules/test-no-clock-polling-for-promise-flips.js +3 -0
- package/dist/rules/tissue-must-compose.d.ts +1 -1
- package/dist/rules/tissue-no-data-props.d.ts +2 -2
- package/dist/rules/tokens-only-colors.d.ts +8 -11
- package/dist/rules/tokens-only-colors.js +36 -7
- package/dist/rules/worker-handles-are-scoped.d.ts +1 -1
- package/dist/timing.d.ts +18 -0
- package/dist/timing.js +75 -0
- 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` |
|
|
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 `
|
|
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 | — |
|
|
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 ·
|
|
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 = [
|
|
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
|
|
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;
|
package/dist/rules/lib/doors.js
CHANGED
|
@@ -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
|
|
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,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
|
+
};
|