@goodbones/oxlint 0.1.0-beta.1 → 0.1.0-beta.10
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/build/dts/campaigns-rule.d.ts +4 -0
- package/build/dts/campaigns-rule.d.ts.map +1 -0
- package/build/dts/config-loader.d.ts +0 -1
- package/build/dts/config-loader.d.ts.map +1 -1
- package/build/dts/exports-rule.d.ts.map +1 -1
- package/build/dts/imports-rule.d.ts.map +1 -1
- package/build/dts/members-rule.d.ts.map +1 -1
- package/build/dts/oxlint-api.d.ts +4 -33
- package/build/dts/oxlint-api.d.ts.map +1 -1
- package/build/dts/plugin.d.ts +1 -0
- package/build/dts/plugin.d.ts.map +1 -1
- package/build/dts/surface-rule.d.ts.map +1 -1
- package/build/esm/campaigns-rule.js +102 -0
- package/build/esm/campaigns-rule.js.map +1 -0
- package/build/esm/config-loader.js +56 -7
- package/build/esm/config-loader.js.map +1 -1
- package/build/esm/exports-rule.js +15 -71
- package/build/esm/exports-rule.js.map +1 -1
- package/build/esm/imports-rule.js +14 -26
- package/build/esm/imports-rule.js.map +1 -1
- package/build/esm/members-rule.js +10 -112
- package/build/esm/members-rule.js.map +1 -1
- package/build/esm/oxlint-api.js +22 -20
- package/build/esm/oxlint-api.js.map +1 -1
- package/build/esm/plugin.js +4 -2
- package/build/esm/plugin.js.map +1 -1
- package/build/esm/surface-rule.js +11 -127
- package/build/esm/surface-rule.js.map +1 -1
- package/package.json +4 -3
- package/src/campaigns-rule.ts +129 -0
- package/src/config-loader.ts +60 -7
- package/src/exports-rule.ts +19 -106
- package/src/imports-rule.ts +16 -35
- package/src/members-rule.ts +11 -165
- package/src/oxlint-api.ts +31 -47
- package/src/plugin.ts +5 -2
- package/src/surface-rule.ts +15 -189
package/src/oxlint-api.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
type ProgramBody,
|
|
5
|
+
type ReadFacts,
|
|
6
|
+
readProgram,
|
|
7
|
+
type SyntaxNode,
|
|
8
|
+
} from "@goodbones/typescript";
|
|
3
9
|
import type { RuleTester } from "oxlint/plugins-dev";
|
|
4
10
|
|
|
5
11
|
// oxlint publishes no plugin types, so the exact `Context`, node and fixer shapes
|
|
@@ -12,55 +18,33 @@ type Diagnostic = Parameters<RuleContext["report"]>[0];
|
|
|
12
18
|
export type ReportableNode = Extract<Diagnostic, { node: unknown }>["node"];
|
|
13
19
|
export type Fixer = Parameters<NonNullable<Diagnostic["fix"]>>[0];
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
// The root of the tree oxlint hands a rule, and the one the pack's reader
|
|
22
|
+
// walks.
|
|
23
|
+
export type Program = RuleContext["sourceCode"]["ast"];
|
|
18
24
|
|
|
19
25
|
export const toRepoRelative = (repoRoot: string, filename: string): string =>
|
|
20
26
|
path.relative(repoRoot, filename).replaceAll(path.sep, "/");
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
readonly moduleReference?: {
|
|
45
|
-
readonly type: string;
|
|
46
|
-
readonly expression?: { readonly value?: unknown } | null;
|
|
47
|
-
} | null;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export const requireSpecifierOf = (node: CallNode): string | null => {
|
|
51
|
-
if (node.callee?.type !== "Identifier" || node.callee.name !== "require") return null;
|
|
52
|
-
const [first] = node.arguments ?? [];
|
|
53
|
-
return first?.type === "Literal" && typeof first.value === "string" ? first.value : null;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export const importExpressionSpecifierOf = (node: ImportExpressionNode): string | null => {
|
|
57
|
-
const source = node.source;
|
|
58
|
-
return source?.type === "Literal" && typeof source.value === "string" ? source.value : null;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export const importEqualsSpecifierOf = (node: ImportEqualsNode): string | null => {
|
|
62
|
-
const reference = node.moduleReference;
|
|
63
|
-
if (reference?.type !== "TSExternalModuleReference") return null;
|
|
64
|
-
const value = reference.expression?.value;
|
|
65
|
-
return typeof value === "string" ? value : null;
|
|
28
|
+
// A diagnostic is placed by the node's range, which every node of oxlint's
|
|
29
|
+
// tree carries; a node from another parse carries `start` and `end`.
|
|
30
|
+
export const at = (node: SyntaxNode): ReportableNode => ({
|
|
31
|
+
range: node.range ?? [node.start, node.end],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// The facts of one file, read once and shared by every rule that runs on it:
|
|
35
|
+
// oxlint deserialises a file's tree once and hands the same `Program` to each
|
|
36
|
+
// rule, so the tree is the key.
|
|
37
|
+
const read = new WeakMap<Program, ReadFacts>();
|
|
38
|
+
|
|
39
|
+
export const factsOfProgram = (file: string, program: Program): ReadFacts => {
|
|
40
|
+
const found = read.get(program);
|
|
41
|
+
if (found !== undefined) return found;
|
|
42
|
+
// oxlint's node types are `@oxc-project/types`' with `parent` required on
|
|
43
|
+
// every node — the same tree, generated from the same source. Each node
|
|
44
|
+
// type assigns on its own; the compiler gives up relating the two
|
|
45
|
+
// ~190-member recursive unions as wholes, so the boundary is asserted once,
|
|
46
|
+
// here. The parity suite is what proves the trees are one.
|
|
47
|
+
const facts = readProgram(file, program as ProgramBody);
|
|
48
|
+
read.set(program, facts);
|
|
49
|
+
return facts;
|
|
66
50
|
};
|
package/src/plugin.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { makeCampaignsRule } from "./campaigns-rule.js";
|
|
2
|
+
import { loadPolicyFromFile } from "./config-loader.js";
|
|
2
3
|
import { makeExportsRule } from "./exports-rule.js";
|
|
3
4
|
import { makeImportsRule } from "./imports-rule.js";
|
|
4
5
|
import { makeMembersRule } from "./members-rule.js";
|
|
@@ -13,7 +14,7 @@ import { makeSurfaceRule } from "./surface-rule.js";
|
|
|
13
14
|
// codebase.
|
|
14
15
|
const policy = await loadPolicyFromFile(
|
|
15
16
|
process.env.ARCHITECTURE_ROOT ?? process.cwd(),
|
|
16
|
-
process.env.ARCHITECTURE_CONFIG
|
|
17
|
+
process.env.ARCHITECTURE_CONFIG,
|
|
17
18
|
);
|
|
18
19
|
|
|
19
20
|
// A manifest written in a shape on its way out still loads; it says so once,
|
|
@@ -28,12 +29,14 @@ export const rules: {
|
|
|
28
29
|
readonly members: OxlintRule;
|
|
29
30
|
readonly structure: OxlintRule;
|
|
30
31
|
readonly surface: OxlintRule;
|
|
32
|
+
readonly campaigns: OxlintRule;
|
|
31
33
|
} = {
|
|
32
34
|
imports: makeImportsRule(policy),
|
|
33
35
|
exports: makeExportsRule(policy),
|
|
34
36
|
members: makeMembersRule(policy),
|
|
35
37
|
structure: makeStructureRule(policy),
|
|
36
38
|
surface: makeSurfaceRule(policy),
|
|
39
|
+
campaigns: makeCampaignsRule(policy),
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
const plugin: { readonly meta: { readonly name: string }; readonly rules: typeof rules } = {
|
package/src/surface-rule.ts
CHANGED
|
@@ -1,193 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type CompiledSurfaceRule,
|
|
3
|
-
type DeclarationKind,
|
|
4
3
|
evaluateSurface,
|
|
5
|
-
type ExportSite,
|
|
6
4
|
formatMessage,
|
|
7
5
|
type LoadedPolicy,
|
|
8
6
|
surfaceRulesSelecting,
|
|
9
7
|
} from "@goodbones/core";
|
|
8
|
+
import type { ReadExportSite } from "@goodbones/typescript";
|
|
10
9
|
|
|
11
10
|
import {
|
|
11
|
+
at,
|
|
12
|
+
factsOfProgram,
|
|
12
13
|
type OxlintRule,
|
|
13
|
-
type
|
|
14
|
+
type Program,
|
|
14
15
|
type RuleContext,
|
|
15
16
|
toRepoRelative,
|
|
16
17
|
} from "./oxlint-api.js";
|
|
17
18
|
|
|
18
|
-
type NamedNode = ReportableNode & {
|
|
19
|
-
readonly type?: string;
|
|
20
|
-
readonly name?: unknown;
|
|
21
|
-
readonly value?: unknown;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// A top-level statement, as far as this rule reads one. Every field is
|
|
25
|
-
// optional because the visitor takes the whole `Node` union; each is checked
|
|
26
|
-
// before use.
|
|
27
|
-
type StatementNode = ReportableNode & {
|
|
28
|
-
readonly type: string;
|
|
29
|
-
readonly id?: NamedNode | null;
|
|
30
|
-
readonly declaration?: StatementNode | null;
|
|
31
|
-
readonly declarations?: ReadonlyArray<{ readonly id?: PatternNode | null }> | null;
|
|
32
|
-
readonly specifiers?: ReadonlyArray<SpecifierNode> | null;
|
|
33
|
-
readonly source?: { readonly value?: unknown } | null;
|
|
34
|
-
readonly exported?: NamedNode | null;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
type PatternNode = ReportableNode & {
|
|
38
|
-
readonly type: string;
|
|
39
|
-
readonly name?: unknown;
|
|
40
|
-
readonly properties?: ReadonlyArray<{
|
|
41
|
-
readonly value?: PatternNode | null;
|
|
42
|
-
readonly argument?: PatternNode | null;
|
|
43
|
-
}> | null;
|
|
44
|
-
readonly elements?: ReadonlyArray<PatternNode | null> | null;
|
|
45
|
-
readonly left?: PatternNode | null;
|
|
46
|
-
readonly argument?: PatternNode | null;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
type SpecifierNode = ReportableNode & {
|
|
50
|
-
readonly exported?: NamedNode | null;
|
|
51
|
-
readonly local?: NamedNode | null;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
type ProgramNode = ReportableNode & { readonly body?: unknown };
|
|
55
|
-
|
|
56
|
-
const isStatementList = (value: unknown): value is ReadonlyArray<StatementNode> =>
|
|
57
|
-
Array.isArray(value);
|
|
58
|
-
|
|
59
|
-
const nameOf = (node: NamedNode | null | undefined): string | null => {
|
|
60
|
-
if (node === null || node === undefined) return null;
|
|
61
|
-
if (typeof node.name === "string") return node.name;
|
|
62
|
-
return typeof node.value === "string" ? node.value : null;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// The identifiers a binding pattern introduces.
|
|
66
|
-
const patternNames = (pattern: PatternNode | null | undefined): ReadonlyArray<string> => {
|
|
67
|
-
if (pattern === null || pattern === undefined) return [];
|
|
68
|
-
switch (pattern.type) {
|
|
69
|
-
case "Identifier":
|
|
70
|
-
return typeof pattern.name === "string" ? [pattern.name] : [];
|
|
71
|
-
case "ObjectPattern":
|
|
72
|
-
return (pattern.properties ?? []).flatMap((property) =>
|
|
73
|
-
patternNames(property.value ?? property.argument),
|
|
74
|
-
);
|
|
75
|
-
case "ArrayPattern":
|
|
76
|
-
return (pattern.elements ?? []).flatMap(patternNames);
|
|
77
|
-
case "AssignmentPattern":
|
|
78
|
-
return patternNames(pattern.left);
|
|
79
|
-
case "RestElement":
|
|
80
|
-
return patternNames(pattern.argument);
|
|
81
|
-
default:
|
|
82
|
-
return [];
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
// The names a declaration statement introduces, with what it declares them as.
|
|
87
|
-
const declaredNamesOf = (
|
|
88
|
-
statement: StatementNode,
|
|
89
|
-
): ReadonlyArray<readonly [string, DeclarationKind]> => {
|
|
90
|
-
const named = (declares: DeclarationKind): ReadonlyArray<readonly [string, DeclarationKind]> => {
|
|
91
|
-
const name = nameOf(statement.id);
|
|
92
|
-
return name === null ? [] : [[name, declares]];
|
|
93
|
-
};
|
|
94
|
-
switch (statement.type) {
|
|
95
|
-
case "FunctionDeclaration":
|
|
96
|
-
return named("function");
|
|
97
|
-
case "ClassDeclaration":
|
|
98
|
-
return named("class");
|
|
99
|
-
case "VariableDeclaration":
|
|
100
|
-
return (statement.declarations ?? []).flatMap((declaration) =>
|
|
101
|
-
patternNames(declaration.id).map((name) => [name, "variable"] as const),
|
|
102
|
-
);
|
|
103
|
-
case "TSTypeAliasDeclaration":
|
|
104
|
-
return named("type");
|
|
105
|
-
case "TSInterfaceDeclaration":
|
|
106
|
-
return named("interface");
|
|
107
|
-
case "TSEnumDeclaration":
|
|
108
|
-
return named("enum");
|
|
109
|
-
case "TSModuleDeclaration":
|
|
110
|
-
return named("other");
|
|
111
|
-
default:
|
|
112
|
-
return [];
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
type Found = { readonly site: ExportSite; readonly node: ReportableNode };
|
|
117
|
-
|
|
118
|
-
// A file's surface, read off the program body rather than by visiting export
|
|
119
|
-
// nodes: an `export` inside a namespace body is that namespace's, not the
|
|
120
|
-
// module's, and reading the top level directly is what says so without a
|
|
121
|
-
// parent pointer. Mirrors the CLI's `exportSitesOf`.
|
|
122
|
-
const surfaceOf = (file: string, body: ReadonlyArray<StatementNode>): ReadonlyArray<Found> => {
|
|
123
|
-
const locals = new Map<string, DeclarationKind>();
|
|
124
|
-
for (const statement of body) {
|
|
125
|
-
const declaration =
|
|
126
|
-
statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration"
|
|
127
|
-
? statement.declaration
|
|
128
|
-
: statement;
|
|
129
|
-
if (declaration === null || declaration === undefined) continue;
|
|
130
|
-
for (const [name, declares] of declaredNamesOf(declaration)) locals.set(name, declares);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const found: Array<Found> = [];
|
|
134
|
-
const site = (
|
|
135
|
-
node: ReportableNode,
|
|
136
|
-
name: string,
|
|
137
|
-
kind: ExportSite["kind"],
|
|
138
|
-
declares: DeclarationKind,
|
|
139
|
-
reexport: boolean,
|
|
140
|
-
): void => {
|
|
141
|
-
found.push({ node, site: { file, name, kind, declares, reexport } });
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
for (const statement of body) {
|
|
145
|
-
switch (statement.type) {
|
|
146
|
-
case "ExportNamedDeclaration": {
|
|
147
|
-
const declaration = statement.declaration;
|
|
148
|
-
if (declaration !== null && declaration !== undefined) {
|
|
149
|
-
for (const [name, declares] of declaredNamesOf(declaration)) {
|
|
150
|
-
site(declaration, name, "named", declares, false);
|
|
151
|
-
}
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
const reexport = typeof statement.source?.value === "string";
|
|
155
|
-
for (const specifier of statement.specifiers ?? []) {
|
|
156
|
-
const name = nameOf(specifier.exported);
|
|
157
|
-
if (name === null) continue;
|
|
158
|
-
const local = nameOf(specifier.local) ?? name;
|
|
159
|
-
const declares = reexport ? "other" : (locals.get(local) ?? "other");
|
|
160
|
-
site(specifier, name, name === "default" ? "default" : "named", declares, reexport);
|
|
161
|
-
}
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
case "ExportDefaultDeclaration": {
|
|
165
|
-
const declaration = statement.declaration;
|
|
166
|
-
const declares: DeclarationKind =
|
|
167
|
-
declaration === null || declaration === undefined
|
|
168
|
-
? "expression"
|
|
169
|
-
: declaration.type === "FunctionDeclaration"
|
|
170
|
-
? "function"
|
|
171
|
-
: declaration.type === "ClassDeclaration"
|
|
172
|
-
? "class"
|
|
173
|
-
: declaration.type === "TSInterfaceDeclaration"
|
|
174
|
-
? "interface"
|
|
175
|
-
: declaration.type === "Identifier"
|
|
176
|
-
? (locals.get(nameOf(declaration) ?? "") ?? "expression")
|
|
177
|
-
: "expression";
|
|
178
|
-
site(statement, "default", "default", declares, false);
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
case "ExportAllDeclaration":
|
|
182
|
-
site(statement, nameOf(statement.exported) ?? "*", "namespace", "other", true);
|
|
183
|
-
break;
|
|
184
|
-
default:
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return found;
|
|
189
|
-
};
|
|
190
|
-
|
|
191
19
|
export const makeSurfaceRule = (policy: LoadedPolicy): OxlintRule => ({
|
|
192
20
|
meta: {
|
|
193
21
|
type: "problem" as const,
|
|
@@ -210,26 +38,24 @@ export const makeSurfaceRule = (policy: LoadedPolicy): OxlintRule => ({
|
|
|
210
38
|
return selected.length > 0;
|
|
211
39
|
},
|
|
212
40
|
|
|
213
|
-
Program(node:
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
const violations = evaluateSurface(
|
|
217
|
-
selected,
|
|
218
|
-
file,
|
|
219
|
-
found.map((one) => one.site),
|
|
220
|
-
);
|
|
41
|
+
Program(node: Program) {
|
|
42
|
+
const sites = factsOfProgram(file, node).exportSites;
|
|
43
|
+
const violations = evaluateSurface(selected, file, sites);
|
|
221
44
|
|
|
222
45
|
// A per-site violation lands on the site's own node; a `count` one, on
|
|
223
46
|
// the program — it is about the file.
|
|
224
|
-
const used = new Set<
|
|
47
|
+
const used = new Set<ReadExportSite>();
|
|
225
48
|
for (const violation of violations) {
|
|
226
49
|
if (policy.baseline.isBaselined(violation)) continue;
|
|
227
|
-
const
|
|
50
|
+
const site =
|
|
228
51
|
violation.subject === null
|
|
229
52
|
? undefined
|
|
230
|
-
:
|
|
231
|
-
if (
|
|
232
|
-
context.report({
|
|
53
|
+
: sites.find((one) => one.name === violation.subject && !used.has(one));
|
|
54
|
+
if (site !== undefined) used.add(site);
|
|
55
|
+
context.report({
|
|
56
|
+
node: site === undefined ? node : at(site.node),
|
|
57
|
+
message: formatMessage(violation),
|
|
58
|
+
});
|
|
233
59
|
}
|
|
234
60
|
},
|
|
235
61
|
};
|