@goodbones/oxlint 0.1.0-beta.2 → 0.1.0-beta.21
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/host.d.ts +19 -0
- package/build/dts/campaigns/host.d.ts.map +1 -0
- package/build/dts/campaigns/live.d.ts +3 -0
- package/build/dts/campaigns/live.d.ts.map +1 -0
- package/build/dts/campaigns/none.d.ts +6 -0
- package/build/dts/campaigns/none.d.ts.map +1 -0
- package/build/dts/campaigns/rule.d.ts +6 -0
- package/build/dts/campaigns/rule.d.ts.map +1 -0
- package/build/dts/config-loader.d.ts +3 -3
- 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/host.js +2 -0
- package/build/esm/campaigns/host.js.map +1 -0
- package/build/esm/campaigns/live.js +81 -0
- package/build/esm/campaigns/live.js.map +1 -0
- package/build/esm/campaigns/none.js +46 -0
- package/build/esm/campaigns/none.js.map +1 -0
- package/build/esm/campaigns/rule.js +163 -0
- package/build/esm/campaigns/rule.js.map +1 -0
- package/build/esm/config-loader.js +37 -5
- 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 -0
- 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 +19 -7
- package/src/campaigns/host.ts +45 -0
- package/src/campaigns/live.ts +106 -0
- package/src/campaigns/none.ts +54 -0
- package/src/campaigns/rule.ts +243 -0
- package/src/config-loader.ts +42 -5
- 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 -0
- package/src/surface-rule.ts +15 -189
package/src/exports-rule.ts
CHANGED
|
@@ -1,97 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type Binding,
|
|
3
2
|
evaluateSelectedBindings,
|
|
4
3
|
exportRulesSelecting,
|
|
5
4
|
formatMessage,
|
|
6
5
|
type LoadedPolicy,
|
|
7
6
|
type SelectedExportRule,
|
|
8
7
|
} from "@goodbones/core";
|
|
8
|
+
import type { ReadBinding, ReadEdge } from "@goodbones/typescript";
|
|
9
9
|
import * as Result from "effect/Result";
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
at,
|
|
13
|
+
factsOfProgram,
|
|
13
14
|
type Fixer,
|
|
14
|
-
type ImportEqualsNode,
|
|
15
|
-
importEqualsSpecifierOf,
|
|
16
|
-
type ImportExpressionNode,
|
|
17
|
-
importExpressionSpecifierOf,
|
|
18
15
|
type OxlintRule,
|
|
19
|
-
type
|
|
20
|
-
requireSpecifierOf,
|
|
16
|
+
type Program,
|
|
21
17
|
type RuleContext,
|
|
22
18
|
toRepoRelative,
|
|
23
19
|
} from "./oxlint-api.js";
|
|
24
20
|
|
|
25
|
-
type NamedNode = ReportableNode & { readonly name?: unknown; readonly value?: unknown };
|
|
26
|
-
|
|
27
|
-
type SpecifierNode = ReportableNode & {
|
|
28
|
-
readonly type: string;
|
|
29
|
-
readonly imported?: NamedNode | null;
|
|
30
|
-
readonly local?: NamedNode | null;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
type DeclarationNode = ReportableNode & {
|
|
34
|
-
readonly source?: { readonly value?: unknown } | null;
|
|
35
|
-
readonly specifiers?: ReadonlyArray<SpecifierNode> | null;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const nameOf = (node: NamedNode | null | undefined): string | null => {
|
|
39
|
-
if (node === null || node === undefined) return null;
|
|
40
|
-
if (typeof node.name === "string") return node.name;
|
|
41
|
-
// `import { "a-b" as ab }` — a string-literal export name.
|
|
42
|
-
return typeof node.value === "string" ? node.value : null;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
type Bound = Binding & { readonly node: ReportableNode; readonly local: string };
|
|
46
|
-
|
|
47
|
-
// The whole module, as one binding. `export * from "m"`, `export * as ns from
|
|
48
|
-
// "m"`, `import x = require("m")`, `import("m")` and `require("m")` all carry
|
|
49
|
-
// every export of `m` at once, exactly as `import * as ns` does — and are the
|
|
50
|
-
// same way around a rule about a name. A side-effect import carries nothing.
|
|
51
|
-
const wholeModule = (node: ReportableNode, local = ""): Bound => ({
|
|
52
|
-
symbol: "*",
|
|
53
|
-
kind: "namespace",
|
|
54
|
-
node,
|
|
55
|
-
local,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
type ExportAllNode = DeclarationNode & { readonly exported?: NamedNode | null };
|
|
59
|
-
|
|
60
|
-
const boundOf = (specifier: SpecifierNode): Bound | null => {
|
|
61
|
-
const local = nameOf(specifier.local) ?? "";
|
|
62
|
-
switch (specifier.type) {
|
|
63
|
-
case "ImportSpecifier": {
|
|
64
|
-
const symbol = nameOf(specifier.imported);
|
|
65
|
-
return symbol === null ? null : { symbol, kind: "named", node: specifier, local };
|
|
66
|
-
}
|
|
67
|
-
case "ImportDefaultSpecifier":
|
|
68
|
-
return { symbol: "default", kind: "default", node: specifier, local };
|
|
69
|
-
case "ImportNamespaceSpecifier":
|
|
70
|
-
return { symbol: "*", kind: "namespace", node: specifier, local };
|
|
71
|
-
// `export { a } from "…"` — `local` is the name in the source module.
|
|
72
|
-
case "ExportSpecifier": {
|
|
73
|
-
const symbol = nameOf(specifier.local);
|
|
74
|
-
return symbol === null ? null : { symbol, kind: "named", node: specifier, local: symbol };
|
|
75
|
-
}
|
|
76
|
-
default:
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const specifierOf = (node: DeclarationNode): string | null => {
|
|
82
|
-
const value = node.source?.value;
|
|
83
|
-
return typeof value === "string" ? value : null;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const boundSpecifiers = (node: DeclarationNode): ReadonlyArray<Bound> =>
|
|
87
|
-
(node.specifiers ?? []).map(boundOf).filter((one): one is Bound => one !== null);
|
|
88
|
-
|
|
89
21
|
// `import { A, B as C } from "pkg"` becomes `import * as A from "pkg/A"` and
|
|
90
22
|
// `import * as C from "pkg/B"`. Only whole-declaration rewrites are offered: a
|
|
91
23
|
// declaration mixing restricted named imports with a default or namespace one
|
|
92
24
|
// would need comma surgery inside the braces, and a fix that is subtly wrong is
|
|
93
25
|
// worse than a diagnostic the author resolves by hand.
|
|
94
|
-
const subpathNamespaceImport = (specifier: string, bound: ReadonlyArray<
|
|
26
|
+
const subpathNamespaceImport = (specifier: string, bound: ReadonlyArray<ReadBinding>): string =>
|
|
95
27
|
bound
|
|
96
28
|
.map((binding) => `import * as ${binding.local} from "${specifier}/${binding.symbol}";`)
|
|
97
29
|
.join("\n");
|
|
@@ -111,20 +43,16 @@ export const makeExportsRule = (policy: LoadedPolicy): OxlintRule => ({
|
|
|
111
43
|
let importer = "";
|
|
112
44
|
let selected: ReadonlyArray<SelectedExportRule> = [];
|
|
113
45
|
|
|
114
|
-
// `
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
const check = (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
bound: ReadonlyArray<Bound>,
|
|
121
|
-
fixable: boolean,
|
|
122
|
-
): void => {
|
|
123
|
-
if (specifierValue === null || bound.length === 0) return;
|
|
46
|
+
// Only an `import` declaration can be rewritten into subpath namespace
|
|
47
|
+
// imports. `export *` and the whole-module forms are reported and left
|
|
48
|
+
// for the author.
|
|
49
|
+
const check = (edge: ReadEdge): void => {
|
|
50
|
+
const { bindings: bound, node, specifier } = edge;
|
|
51
|
+
if (bound.length === 0) return;
|
|
124
52
|
|
|
125
53
|
const outcome = evaluateSelectedBindings(selected, policy.resolver, {
|
|
126
54
|
importer,
|
|
127
|
-
specifier
|
|
55
|
+
specifier,
|
|
128
56
|
bindings: bound,
|
|
129
57
|
});
|
|
130
58
|
|
|
@@ -136,30 +64,30 @@ export const makeExportsRule = (policy: LoadedPolicy): OxlintRule => ({
|
|
|
136
64
|
|
|
137
65
|
for (const { bindings, rule, violation } of outcome.success) {
|
|
138
66
|
if (policy.baseline.isBaselined(violation)) continue;
|
|
139
|
-
const offending = bound.filter((one
|
|
67
|
+
const offending = bound.filter((one) =>
|
|
140
68
|
bindings.some((binding) => binding.symbol === one.symbol && binding.kind === one.kind),
|
|
141
69
|
);
|
|
142
70
|
// The rewrite is `import * as X from "pkg/<name>"`, which only means
|
|
143
71
|
// something for a named binding — a namespace one has no name to put
|
|
144
72
|
// in the subpath.
|
|
145
73
|
const rewritable =
|
|
146
|
-
|
|
74
|
+
edge.form === "import" &&
|
|
147
75
|
rule.fix === "subpath-namespace-import" &&
|
|
148
76
|
offending.length === bound.length &&
|
|
149
77
|
offending.every((one) => one.kind === "named");
|
|
150
78
|
|
|
151
79
|
if (rewritable) {
|
|
152
80
|
context.report({
|
|
153
|
-
node,
|
|
81
|
+
node: at(node),
|
|
154
82
|
message: formatMessage(violation),
|
|
155
83
|
fix: (fixer: Fixer) =>
|
|
156
|
-
fixer.replaceText(node, subpathNamespaceImport(
|
|
84
|
+
fixer.replaceText(at(node), subpathNamespaceImport(specifier, offending)),
|
|
157
85
|
});
|
|
158
86
|
continue;
|
|
159
87
|
}
|
|
160
88
|
|
|
161
89
|
context.report({
|
|
162
|
-
node: offending[0]?.node ?? node,
|
|
90
|
+
node: at(offending[0]?.node ?? node),
|
|
163
91
|
message: formatMessage(violation),
|
|
164
92
|
});
|
|
165
93
|
}
|
|
@@ -172,23 +100,8 @@ export const makeExportsRule = (policy: LoadedPolicy): OxlintRule => ({
|
|
|
172
100
|
selected = exportRulesSelecting(policy.exportRules, importer);
|
|
173
101
|
return selected.length > 0;
|
|
174
102
|
},
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
},
|
|
178
|
-
ExportNamedDeclaration(node: DeclarationNode) {
|
|
179
|
-
check(node, specifierOf(node), boundSpecifiers(node), false);
|
|
180
|
-
},
|
|
181
|
-
ExportAllDeclaration(node: ExportAllNode) {
|
|
182
|
-
check(node, specifierOf(node), [wholeModule(node, nameOf(node.exported) ?? "")], false);
|
|
183
|
-
},
|
|
184
|
-
ImportExpression(node: ImportExpressionNode) {
|
|
185
|
-
check(node, importExpressionSpecifierOf(node), [wholeModule(node)], false);
|
|
186
|
-
},
|
|
187
|
-
CallExpression(node: CallNode) {
|
|
188
|
-
check(node, requireSpecifierOf(node), [wholeModule(node)], false);
|
|
189
|
-
},
|
|
190
|
-
TSImportEqualsDeclaration(node: ImportEqualsNode) {
|
|
191
|
-
check(node, importEqualsSpecifierOf(node), [wholeModule(node)], false);
|
|
103
|
+
Program(node: Program) {
|
|
104
|
+
for (const edge of factsOfProgram(importer, node).edges) check(edge);
|
|
192
105
|
},
|
|
193
106
|
};
|
|
194
107
|
},
|
package/src/imports-rule.ts
CHANGED
|
@@ -5,20 +5,15 @@ import {
|
|
|
5
5
|
rulesSelecting,
|
|
6
6
|
type SelectedRule,
|
|
7
7
|
} from "@goodbones/core";
|
|
8
|
+
import type { ReadEdge } from "@goodbones/typescript";
|
|
8
9
|
import * as Result from "effect/Result";
|
|
9
10
|
|
|
10
11
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
importEqualsSpecifierOf,
|
|
14
|
-
type ImportExpressionNode,
|
|
15
|
-
importExpressionSpecifierOf,
|
|
12
|
+
at,
|
|
13
|
+
factsOfProgram,
|
|
16
14
|
type OxlintRule,
|
|
17
|
-
type
|
|
18
|
-
requireSpecifierOf,
|
|
15
|
+
type Program,
|
|
19
16
|
type RuleContext,
|
|
20
|
-
type SourceNode,
|
|
21
|
-
specifierOf,
|
|
22
17
|
toRepoRelative,
|
|
23
18
|
} from "./oxlint-api.js";
|
|
24
19
|
|
|
@@ -44,32 +39,29 @@ export const makeImportsRule = (policy: LoadedPolicy): OxlintRule => ({
|
|
|
44
39
|
let importer = "";
|
|
45
40
|
let selected: ReadonlyArray<SelectedRule> = [];
|
|
46
41
|
|
|
47
|
-
const check = (
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
const check = (edge: ReadEdge): void => {
|
|
43
|
+
const { specifier } = edge;
|
|
50
44
|
const outcome = evaluateSelectedEdge(selected, policy.resolver, { importer, specifier });
|
|
51
45
|
|
|
52
46
|
if (Result.isFailure(outcome)) {
|
|
53
47
|
if (policy.config.resolve.unresolved === "off") return;
|
|
54
48
|
if (policy.ignoreUnresolved.some((pattern) => pattern.test(specifier))) return;
|
|
55
|
-
context.report({
|
|
49
|
+
context.report({
|
|
50
|
+
node: at(edge.node),
|
|
51
|
+
message: unresolvedMessage(specifier, outcome.failure.detail),
|
|
52
|
+
});
|
|
56
53
|
return;
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
for (const violation of outcome.success) {
|
|
60
57
|
if (policy.baseline.isBaselined(violation)) continue;
|
|
61
|
-
context.report({ node, message: formatMessage(violation) });
|
|
58
|
+
context.report({ node: at(edge.node), message: formatMessage(violation) });
|
|
62
59
|
}
|
|
63
60
|
};
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Every form that names a module is an edge. The CLI adapter reads the same
|
|
70
|
-
// five out of TypeScript's tree, and the parity suite holds the two to it: a
|
|
71
|
-
// `require` the plugin skipped would be a rule that enforces nothing under
|
|
72
|
-
// `oxlint` while failing under `architecture check`.
|
|
62
|
+
// Every form that names a module is an edge, and the pack's reader is
|
|
63
|
+
// what says which forms those are — the same reader the CLI reads a file
|
|
64
|
+
// through, so a form one host sees the other sees too.
|
|
73
65
|
return {
|
|
74
66
|
before() {
|
|
75
67
|
importer = toRepoRelative(policy.repoRoot, context.filename);
|
|
@@ -77,19 +69,8 @@ export const makeImportsRule = (policy: LoadedPolicy): OxlintRule => ({
|
|
|
77
69
|
selected = rulesSelecting(policy.importRules, importer);
|
|
78
70
|
return selected.length > 0;
|
|
79
71
|
},
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
ExportAllDeclaration: checkSource,
|
|
83
|
-
// `import("m")` with a literal argument. A computed one is not a fact a
|
|
84
|
-
// static policy can speak about, in either adapter.
|
|
85
|
-
ImportExpression(node: ImportExpressionNode) {
|
|
86
|
-
check(node, importExpressionSpecifierOf(node));
|
|
87
|
-
},
|
|
88
|
-
CallExpression(node: CallNode) {
|
|
89
|
-
check(node, requireSpecifierOf(node));
|
|
90
|
-
},
|
|
91
|
-
TSImportEqualsDeclaration(node: ImportEqualsNode) {
|
|
92
|
-
check(node, importEqualsSpecifierOf(node));
|
|
72
|
+
Program(node: Program) {
|
|
73
|
+
for (const edge of factsOfProgram(importer, node).edges) check(edge);
|
|
93
74
|
},
|
|
94
75
|
};
|
|
95
76
|
},
|
package/src/members-rule.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type CompiledMemberRule,
|
|
3
|
-
type DeclarationKind,
|
|
4
3
|
evaluateMemberSite,
|
|
5
4
|
formatMessage,
|
|
6
5
|
type LoadedPolicy,
|
|
@@ -8,112 +7,14 @@ import {
|
|
|
8
7
|
} from "@goodbones/core";
|
|
9
8
|
|
|
10
9
|
import {
|
|
10
|
+
at,
|
|
11
|
+
factsOfProgram,
|
|
11
12
|
type OxlintRule,
|
|
12
|
-
type
|
|
13
|
+
type Program,
|
|
13
14
|
type RuleContext,
|
|
14
15
|
toRepoRelative,
|
|
15
16
|
} from "./oxlint-api.js";
|
|
16
17
|
|
|
17
|
-
type NamedNode = ReportableNode & {
|
|
18
|
-
readonly type: string;
|
|
19
|
-
readonly name?: unknown;
|
|
20
|
-
readonly value?: unknown;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type MemberNode = ReportableNode & {
|
|
24
|
-
readonly type: string;
|
|
25
|
-
readonly key?: (NamedNode & { readonly type?: string }) | null;
|
|
26
|
-
readonly computed?: boolean;
|
|
27
|
-
// A class method's kind: `method`, `get`, `set` or `constructor`.
|
|
28
|
-
readonly kind?: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// A type, as far as this rule reads it: a literal with members, or an
|
|
32
|
-
// intersection / union / parenthesised type wrapping others.
|
|
33
|
-
type TypeNode = ReportableNode & {
|
|
34
|
-
readonly type: string;
|
|
35
|
-
readonly members?: ReadonlyArray<MemberNode>;
|
|
36
|
-
readonly types?: ReadonlyArray<TypeNode>;
|
|
37
|
-
readonly typeAnnotation?: TypeNode | null;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
type TypeAliasNode = ReportableNode & {
|
|
41
|
-
readonly id?: NamedNode | null;
|
|
42
|
-
readonly typeAnnotation?: TypeNode | null;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// Every visitor takes the whole `Node` union, so `body` is typed to admit a
|
|
46
|
-
// function body as readily as an interface's, and narrowed below.
|
|
47
|
-
type InterfaceNode = ReportableNode & {
|
|
48
|
-
readonly id?: NamedNode | null;
|
|
49
|
-
readonly body?: { readonly type?: string; readonly body?: unknown } | null;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
type ClassNode = InterfaceNode;
|
|
53
|
-
|
|
54
|
-
const isMemberList = (value: unknown): value is ReadonlyArray<MemberNode> => Array.isArray(value);
|
|
55
|
-
|
|
56
|
-
type CallNode = ReportableNode & {
|
|
57
|
-
readonly callee?:
|
|
58
|
-
| (ReportableNode & {
|
|
59
|
-
readonly type: string;
|
|
60
|
-
readonly name?: unknown;
|
|
61
|
-
readonly computed?: boolean;
|
|
62
|
-
readonly property?: (NamedNode & { readonly type: string }) | null;
|
|
63
|
-
})
|
|
64
|
-
| null;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// The member shapes that carry a name a vocabulary rule can speak about: a
|
|
68
|
-
// property or method signature in a type, a property, method or accessor in a
|
|
69
|
-
// class. Mirrors the CLI's `isNamedMember`.
|
|
70
|
-
const DECLARED_MEMBER_TYPES = new Set([
|
|
71
|
-
"TSPropertySignature",
|
|
72
|
-
"TSMethodSignature",
|
|
73
|
-
"PropertyDefinition",
|
|
74
|
-
"MethodDefinition",
|
|
75
|
-
"TSAbstractPropertyDefinition",
|
|
76
|
-
"TSAbstractMethodDefinition",
|
|
77
|
-
]);
|
|
78
|
-
|
|
79
|
-
// The type literals written in a type, through intersections, unions and
|
|
80
|
-
// parentheses. A reference is not followed: its members are declared where it
|
|
81
|
-
// is, and reported there under its own name. Mirrors the CLI's `literalsOf`.
|
|
82
|
-
const literalsOf = (node: TypeNode | null | undefined): ReadonlyArray<TypeNode> => {
|
|
83
|
-
if (node === null || node === undefined) return [];
|
|
84
|
-
switch (node.type) {
|
|
85
|
-
case "TSTypeLiteral":
|
|
86
|
-
return [node];
|
|
87
|
-
case "TSIntersectionType":
|
|
88
|
-
case "TSUnionType":
|
|
89
|
-
return (node.types ?? []).flatMap(literalsOf);
|
|
90
|
-
case "TSParenthesizedType":
|
|
91
|
-
return literalsOf(node.typeAnnotation);
|
|
92
|
-
default:
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const nameOf = (node: NamedNode | null | undefined): string | null => {
|
|
98
|
-
if (node === null || node === undefined) return null;
|
|
99
|
-
if (typeof node.name === "string") return node.name;
|
|
100
|
-
return typeof node.value === "string" ? node.value : null;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
// The name a call is made by: `f()` and `x.f()` are both `f`. `x[f]()` and
|
|
104
|
-
// `x["f"]()` are not — a computed property is not a name a vocabulary rule can
|
|
105
|
-
// speak about — and neither is a private `x.#f()`. The CLI reads the same
|
|
106
|
-
// three cases out of TypeScript's tree; the parity suite keeps them agreeing.
|
|
107
|
-
const calleeName = (node: CallNode): string | null => {
|
|
108
|
-
const callee = node.callee;
|
|
109
|
-
if (callee === null || callee === undefined) return null;
|
|
110
|
-
if (callee.type === "Identifier" && typeof callee.name === "string") return callee.name;
|
|
111
|
-
if (callee.type !== "MemberExpression" || callee.computed === true) return null;
|
|
112
|
-
const property = callee.property;
|
|
113
|
-
if (property?.type !== "Identifier") return null;
|
|
114
|
-
return nameOf(property);
|
|
115
|
-
};
|
|
116
|
-
|
|
117
18
|
export const makeMembersRule = (policy: LoadedPolicy): OxlintRule => ({
|
|
118
19
|
meta: {
|
|
119
20
|
type: "problem" as const,
|
|
@@ -128,42 +29,8 @@ export const makeMembersRule = (policy: LoadedPolicy): OxlintRule => ({
|
|
|
128
29
|
let file = "";
|
|
129
30
|
let selected: ReadonlyArray<CompiledMemberRule> = [];
|
|
130
31
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
name: string,
|
|
134
|
-
declaration?: { readonly name: string; readonly declares: DeclarationKind },
|
|
135
|
-
): void => {
|
|
136
|
-
const violations = evaluateMemberSite(selected, {
|
|
137
|
-
file,
|
|
138
|
-
subject: declaration === undefined ? "calls" : "members",
|
|
139
|
-
name,
|
|
140
|
-
...(declaration === undefined
|
|
141
|
-
? {}
|
|
142
|
-
: { in: declaration.name, declares: declaration.declares }),
|
|
143
|
-
});
|
|
144
|
-
for (const violation of violations) {
|
|
145
|
-
if (policy.baseline.isBaselined(violation)) continue;
|
|
146
|
-
context.report({ node, message: formatMessage(violation) });
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// The members written in a declaration, under that declaration's name and
|
|
151
|
-
// kind. A computed key is not a name a vocabulary rule can speak about;
|
|
152
|
-
// neither is a private `#name`, an index, call or construct signature, or
|
|
153
|
-
// a constructor.
|
|
154
|
-
const declared = (
|
|
155
|
-
declaration: string,
|
|
156
|
-
declares: DeclarationKind,
|
|
157
|
-
members: ReadonlyArray<MemberNode>,
|
|
158
|
-
): void => {
|
|
159
|
-
for (const member of members) {
|
|
160
|
-
if (member.computed === true || !DECLARED_MEMBER_TYPES.has(member.type)) continue;
|
|
161
|
-
if (member.key?.type === "PrivateIdentifier" || member.kind === "constructor") continue;
|
|
162
|
-
const name = nameOf(member.key);
|
|
163
|
-
if (name !== null) report(member.key ?? member, name, { name: declaration, declares });
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
|
|
32
|
+
// A declared member is reported at its key, a call at the call — the nodes
|
|
33
|
+
// the pack's reader hands back with each site.
|
|
167
34
|
return {
|
|
168
35
|
before() {
|
|
169
36
|
file = toRepoRelative(policy.repoRoot, context.filename);
|
|
@@ -171,35 +38,14 @@ export const makeMembersRule = (policy: LoadedPolicy): OxlintRule => ({
|
|
|
171
38
|
selected = memberRulesSelecting(policy.memberRules, file);
|
|
172
39
|
return selected.length > 0;
|
|
173
40
|
},
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
41
|
+
Program(node: Program) {
|
|
42
|
+
for (const site of factsOfProgram(file, node).memberSites) {
|
|
43
|
+
for (const violation of evaluateMemberSite(selected, site)) {
|
|
44
|
+
if (policy.baseline.isBaselined(violation)) continue;
|
|
45
|
+
context.report({ node: at(site.node), message: formatMessage(violation) });
|
|
46
|
+
}
|
|
180
47
|
}
|
|
181
48
|
},
|
|
182
|
-
|
|
183
|
-
TSInterfaceDeclaration(node: InterfaceNode) {
|
|
184
|
-
const declaration = nameOf(node.id);
|
|
185
|
-
const members = node.body?.body;
|
|
186
|
-
if (declaration === null || !isMemberList(members)) return;
|
|
187
|
-
declared(declaration, "interface", members);
|
|
188
|
-
},
|
|
189
|
-
|
|
190
|
-
// A named class only, as the CLI reads it: an anonymous default class has
|
|
191
|
-
// no name for `in`, and a class expression is a value.
|
|
192
|
-
ClassDeclaration(node: ClassNode) {
|
|
193
|
-
const declaration = nameOf(node.id);
|
|
194
|
-
const members = node.body?.body;
|
|
195
|
-
if (declaration === null || !isMemberList(members)) return;
|
|
196
|
-
declared(declaration, "class", members);
|
|
197
|
-
},
|
|
198
|
-
|
|
199
|
-
CallExpression(node: CallNode) {
|
|
200
|
-
const name = calleeName(node);
|
|
201
|
-
if (name !== null) report(node, name);
|
|
202
|
-
},
|
|
203
49
|
};
|
|
204
50
|
},
|
|
205
51
|
});
|
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
|
@@ -28,12 +28,17 @@ export const rules: {
|
|
|
28
28
|
readonly members: OxlintRule;
|
|
29
29
|
readonly structure: OxlintRule;
|
|
30
30
|
readonly surface: OxlintRule;
|
|
31
|
+
readonly campaigns: OxlintRule;
|
|
31
32
|
} = {
|
|
32
33
|
imports: makeImportsRule(policy),
|
|
33
34
|
exports: makeExportsRule(policy),
|
|
34
35
|
members: makeMembersRule(policy),
|
|
35
36
|
structure: makeStructureRule(policy),
|
|
36
37
|
surface: makeSurfaceRule(policy),
|
|
38
|
+
// From the campaigns family the plugin was composed with: the rule over
|
|
39
|
+
// the ledgers when `@goodbones/campaigns` is installed, and one that says
|
|
40
|
+
// to install it when it is not.
|
|
41
|
+
campaigns: policy.campaigns.rule(policy),
|
|
37
42
|
};
|
|
38
43
|
|
|
39
44
|
const plugin: { readonly meta: { readonly name: string }; readonly rules: typeof rules } = {
|