@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
|
@@ -1,59 +1,5 @@
|
|
|
1
1
|
import { evaluateMemberSite, formatMessage, memberRulesSelecting, } from "@goodbones/core";
|
|
2
|
-
import { toRepoRelative, } from "./oxlint-api.js";
|
|
3
|
-
const isMemberList = (value) => Array.isArray(value);
|
|
4
|
-
// The member shapes that carry a name a vocabulary rule can speak about: a
|
|
5
|
-
// property or method signature in a type, a property, method or accessor in a
|
|
6
|
-
// class. Mirrors the CLI's `isNamedMember`.
|
|
7
|
-
const DECLARED_MEMBER_TYPES = new Set([
|
|
8
|
-
"TSPropertySignature",
|
|
9
|
-
"TSMethodSignature",
|
|
10
|
-
"PropertyDefinition",
|
|
11
|
-
"MethodDefinition",
|
|
12
|
-
"TSAbstractPropertyDefinition",
|
|
13
|
-
"TSAbstractMethodDefinition",
|
|
14
|
-
]);
|
|
15
|
-
// The type literals written in a type, through intersections, unions and
|
|
16
|
-
// parentheses. A reference is not followed: its members are declared where it
|
|
17
|
-
// is, and reported there under its own name. Mirrors the CLI's `literalsOf`.
|
|
18
|
-
const literalsOf = (node) => {
|
|
19
|
-
if (node === null || node === undefined)
|
|
20
|
-
return [];
|
|
21
|
-
switch (node.type) {
|
|
22
|
-
case "TSTypeLiteral":
|
|
23
|
-
return [node];
|
|
24
|
-
case "TSIntersectionType":
|
|
25
|
-
case "TSUnionType":
|
|
26
|
-
return (node.types ?? []).flatMap(literalsOf);
|
|
27
|
-
case "TSParenthesizedType":
|
|
28
|
-
return literalsOf(node.typeAnnotation);
|
|
29
|
-
default:
|
|
30
|
-
return [];
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
const nameOf = (node) => {
|
|
34
|
-
if (node === null || node === undefined)
|
|
35
|
-
return null;
|
|
36
|
-
if (typeof node.name === "string")
|
|
37
|
-
return node.name;
|
|
38
|
-
return typeof node.value === "string" ? node.value : null;
|
|
39
|
-
};
|
|
40
|
-
// The name a call is made by: `f()` and `x.f()` are both `f`. `x[f]()` and
|
|
41
|
-
// `x["f"]()` are not — a computed property is not a name a vocabulary rule can
|
|
42
|
-
// speak about — and neither is a private `x.#f()`. The CLI reads the same
|
|
43
|
-
// three cases out of TypeScript's tree; the parity suite keeps them agreeing.
|
|
44
|
-
const calleeName = (node) => {
|
|
45
|
-
const callee = node.callee;
|
|
46
|
-
if (callee === null || callee === undefined)
|
|
47
|
-
return null;
|
|
48
|
-
if (callee.type === "Identifier" && typeof callee.name === "string")
|
|
49
|
-
return callee.name;
|
|
50
|
-
if (callee.type !== "MemberExpression" || callee.computed === true)
|
|
51
|
-
return null;
|
|
52
|
-
const property = callee.property;
|
|
53
|
-
if (property?.type !== "Identifier")
|
|
54
|
-
return null;
|
|
55
|
-
return nameOf(property);
|
|
56
|
-
};
|
|
2
|
+
import { at, factsOfProgram, toRepoRelative, } from "./oxlint-api.js";
|
|
57
3
|
export const makeMembersRule = (policy) => ({
|
|
58
4
|
meta: {
|
|
59
5
|
type: "problem",
|
|
@@ -65,36 +11,8 @@ export const makeMembersRule = (policy) => ({
|
|
|
65
11
|
createOnce(context) {
|
|
66
12
|
let file = "";
|
|
67
13
|
let selected = [];
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
file,
|
|
71
|
-
subject: declaration === undefined ? "calls" : "members",
|
|
72
|
-
name,
|
|
73
|
-
...(declaration === undefined
|
|
74
|
-
? {}
|
|
75
|
-
: { in: declaration.name, declares: declaration.declares }),
|
|
76
|
-
});
|
|
77
|
-
for (const violation of violations) {
|
|
78
|
-
if (policy.baseline.isBaselined(violation))
|
|
79
|
-
continue;
|
|
80
|
-
context.report({ node, message: formatMessage(violation) });
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
// The members written in a declaration, under that declaration's name and
|
|
84
|
-
// kind. A computed key is not a name a vocabulary rule can speak about;
|
|
85
|
-
// neither is a private `#name`, an index, call or construct signature, or
|
|
86
|
-
// a constructor.
|
|
87
|
-
const declared = (declaration, declares, members) => {
|
|
88
|
-
for (const member of members) {
|
|
89
|
-
if (member.computed === true || !DECLARED_MEMBER_TYPES.has(member.type))
|
|
90
|
-
continue;
|
|
91
|
-
if (member.key?.type === "PrivateIdentifier" || member.kind === "constructor")
|
|
92
|
-
continue;
|
|
93
|
-
const name = nameOf(member.key);
|
|
94
|
-
if (name !== null)
|
|
95
|
-
report(member.key ?? member, name, { name: declaration, declares });
|
|
96
|
-
}
|
|
97
|
-
};
|
|
14
|
+
// A declared member is reported at its key, a call at the call — the nodes
|
|
15
|
+
// the pack's reader hands back with each site.
|
|
98
16
|
return {
|
|
99
17
|
before() {
|
|
100
18
|
file = toRepoRelative(policy.repoRoot, context.filename);
|
|
@@ -103,35 +21,15 @@ export const makeMembersRule = (policy) => ({
|
|
|
103
21
|
selected = memberRulesSelecting(policy.memberRules, file);
|
|
104
22
|
return selected.length > 0;
|
|
105
23
|
},
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
24
|
+
Program(node) {
|
|
25
|
+
for (const site of factsOfProgram(file, node).memberSites) {
|
|
26
|
+
for (const violation of evaluateMemberSite(selected, site)) {
|
|
27
|
+
if (policy.baseline.isBaselined(violation))
|
|
28
|
+
continue;
|
|
29
|
+
context.report({ node: at(site.node), message: formatMessage(violation) });
|
|
30
|
+
}
|
|
112
31
|
}
|
|
113
32
|
},
|
|
114
|
-
TSInterfaceDeclaration(node) {
|
|
115
|
-
const declaration = nameOf(node.id);
|
|
116
|
-
const members = node.body?.body;
|
|
117
|
-
if (declaration === null || !isMemberList(members))
|
|
118
|
-
return;
|
|
119
|
-
declared(declaration, "interface", members);
|
|
120
|
-
},
|
|
121
|
-
// A named class only, as the CLI reads it: an anonymous default class has
|
|
122
|
-
// no name for `in`, and a class expression is a value.
|
|
123
|
-
ClassDeclaration(node) {
|
|
124
|
-
const declaration = nameOf(node.id);
|
|
125
|
-
const members = node.body?.body;
|
|
126
|
-
if (declaration === null || !isMemberList(members))
|
|
127
|
-
return;
|
|
128
|
-
declared(declaration, "class", members);
|
|
129
|
-
},
|
|
130
|
-
CallExpression(node) {
|
|
131
|
-
const name = calleeName(node);
|
|
132
|
-
if (name !== null)
|
|
133
|
-
report(node, name);
|
|
134
|
-
},
|
|
135
33
|
};
|
|
136
34
|
},
|
|
137
35
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"members-rule.js","sourceRoot":"","sources":["../../src/members-rule.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"members-rule.js","sourceRoot":"","sources":["../../src/members-rule.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,kBAAkB,EAClB,aAAa,EAEb,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,EAAE,EACF,cAAc,EAId,cAAc,GACf,MAAM,iBAAiB,CAAC;AAEzB,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAoB,EAAc,EAAE,CAAC,CAAC;IACpE,IAAI,EAAE;QACJ,IAAI,EAAE,SAAkB;QACxB,IAAI,EAAE;YACJ,WAAW,EACT,6FAA6F;SAChG;QACD,MAAM,EAAE,EAAE;KACX;IAED,UAAU,CAAC,OAAoB;QAC7B,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,IAAI,QAAQ,GAAsC,EAAE,CAAC;QAErD,2EAA2E;QAC3E,+CAA+C;QAC/C,OAAO;YACL,MAAM;gBACJ,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;gBACzD,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;oBAAE,OAAO,KAAK,CAAC;gBACxC,QAAQ,GAAG,oBAAoB,CAAC,MAAM,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;gBAC1D,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YAC7B,CAAC;YACD,OAAO,CAAC,IAAa;gBACnB,KAAK,MAAM,IAAI,IAAI,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;oBAC1D,KAAK,MAAM,SAAS,IAAI,kBAAkB,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC;wBAC3D,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,SAAS,CAAC;4BAAE,SAAS;wBACrD,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;oBAC7E,CAAC;gBACH,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
package/build/esm/oxlint-api.js
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { readProgram, } from "@goodbones/typescript";
|
|
2
3
|
export const toRepoRelative = (repoRoot, filename) => path.relative(repoRoot, filename).replaceAll(path.sep, "/");
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
4
|
+
// A diagnostic is placed by the node's range, which every node of oxlint's
|
|
5
|
+
// tree carries; a node from another parse carries `start` and `end`.
|
|
6
|
+
export const at = (node) => ({
|
|
7
|
+
range: node.range ?? [node.start, node.end],
|
|
8
|
+
});
|
|
9
|
+
// The facts of one file, read once and shared by every rule that runs on it:
|
|
10
|
+
// oxlint deserialises a file's tree once and hands the same `Program` to each
|
|
11
|
+
// rule, so the tree is the key.
|
|
12
|
+
const read = new WeakMap();
|
|
13
|
+
export const factsOfProgram = (file, program) => {
|
|
14
|
+
const found = read.get(program);
|
|
15
|
+
if (found !== undefined)
|
|
16
|
+
return found;
|
|
17
|
+
// oxlint's node types are `@oxc-project/types`' with `parent` required on
|
|
18
|
+
// every node — the same tree, generated from the same source. Each node
|
|
19
|
+
// type assigns on its own; the compiler gives up relating the two
|
|
20
|
+
// ~190-member recursive unions as wholes, so the boundary is asserted once,
|
|
21
|
+
// here. The parity suite is what proves the trees are one.
|
|
22
|
+
const facts = readProgram(file, program);
|
|
23
|
+
read.set(program, facts);
|
|
24
|
+
return facts;
|
|
23
25
|
};
|
|
24
26
|
//# sourceMappingURL=oxlint-api.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oxlint-api.js","sourceRoot":"","sources":["../../src/oxlint-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"oxlint-api.js","sourceRoot":"","sources":["../../src/oxlint-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAGL,WAAW,GAEZ,MAAM,uBAAuB,CAAC;AAiB/B,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,QAAgB,EAAE,QAAgB,EAAU,EAAE,CAC3E,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AAE9D,2EAA2E;AAC3E,qEAAqE;AACrE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAgB,EAAkB,EAAE,CAAC,CAAC;IACvD,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC;CAC5C,CAAC,CAAC;AAEH,6EAA6E;AAC7E,8EAA8E;AAC9E,gCAAgC;AAChC,MAAM,IAAI,GAAG,IAAI,OAAO,EAAsB,CAAC;AAE/C,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,IAAY,EAAE,OAAgB,EAAa,EAAE;IAC1E,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAChC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,0EAA0E;IAC1E,wEAAwE;IACxE,kEAAkE;IAClE,4EAA4E;IAC5E,2DAA2D;IAC3D,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,EAAE,OAAsB,CAAC,CAAC;IACxD,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACzB,OAAO,KAAK,CAAC;AACf,CAAC,CAAC"}
|
package/build/esm/plugin.js
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";
|
|
@@ -9,7 +10,7 @@ import { makeSurfaceRule } from "./surface-rule.js";
|
|
|
9
10
|
// out of the import and fails the run — which is the point: a plugin that came
|
|
10
11
|
// up with no policy would report nothing and be indistinguishable from a clean
|
|
11
12
|
// codebase.
|
|
12
|
-
const policy = await loadPolicyFromFile(process.env.ARCHITECTURE_ROOT ?? process.cwd(), process.env.ARCHITECTURE_CONFIG
|
|
13
|
+
const policy = await loadPolicyFromFile(process.env.ARCHITECTURE_ROOT ?? process.cwd(), process.env.ARCHITECTURE_CONFIG);
|
|
13
14
|
// A manifest written in a shape on its way out still loads; it says so once,
|
|
14
15
|
// here, rather than on every file.
|
|
15
16
|
for (const notice of policy.notices) {
|
|
@@ -21,6 +22,7 @@ export const rules = {
|
|
|
21
22
|
members: makeMembersRule(policy),
|
|
22
23
|
structure: makeStructureRule(policy),
|
|
23
24
|
surface: makeSurfaceRule(policy),
|
|
25
|
+
campaigns: makeCampaignsRule(policy),
|
|
24
26
|
};
|
|
25
27
|
const plugin = {
|
|
26
28
|
meta: { name: "architecture" },
|
package/build/esm/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,gFAAgF;AAChF,gFAAgF;AAChF,+EAA+E;AAC/E,+EAA+E;AAC/E,YAAY;AACZ,MAAM,MAAM,GAAG,MAAM,kBAAkB,CACrC,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,OAAO,CAAC,GAAG,EAAE,EAC9C,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAChC,CAAC;AAEF,6EAA6E;AAC7E,mCAAmC;AACnC,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;IACpC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,8BAA8B,MAAM,IAAI,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,CAAC,MAAM,KAAK,GAOd;IACF,OAAO,EAAE,eAAe,CAAC,MAAM,CAAC;IAChC,OAAO,EAAE,eAAe,CAAC,MAAM,CAAC;IAChC,OAAO,EAAE,eAAe,CAAC,MAAM,CAAC;IAChC,SAAS,EAAE,iBAAiB,CAAC,MAAM,CAAC;IACpC,OAAO,EAAE,eAAe,CAAC,MAAM,CAAC;IAChC,SAAS,EAAE,iBAAiB,CAAC,MAAM,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,GAA+E;IACzF,IAAI,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE;IAC9B,KAAK;CACN,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
|
@@ -1,122 +1,5 @@
|
|
|
1
1
|
import { evaluateSurface, formatMessage, surfaceRulesSelecting, } from "@goodbones/core";
|
|
2
|
-
import { toRepoRelative, } from "./oxlint-api.js";
|
|
3
|
-
const isStatementList = (value) => Array.isArray(value);
|
|
4
|
-
const nameOf = (node) => {
|
|
5
|
-
if (node === null || node === undefined)
|
|
6
|
-
return null;
|
|
7
|
-
if (typeof node.name === "string")
|
|
8
|
-
return node.name;
|
|
9
|
-
return typeof node.value === "string" ? node.value : null;
|
|
10
|
-
};
|
|
11
|
-
// The identifiers a binding pattern introduces.
|
|
12
|
-
const patternNames = (pattern) => {
|
|
13
|
-
if (pattern === null || pattern === undefined)
|
|
14
|
-
return [];
|
|
15
|
-
switch (pattern.type) {
|
|
16
|
-
case "Identifier":
|
|
17
|
-
return typeof pattern.name === "string" ? [pattern.name] : [];
|
|
18
|
-
case "ObjectPattern":
|
|
19
|
-
return (pattern.properties ?? []).flatMap((property) => patternNames(property.value ?? property.argument));
|
|
20
|
-
case "ArrayPattern":
|
|
21
|
-
return (pattern.elements ?? []).flatMap(patternNames);
|
|
22
|
-
case "AssignmentPattern":
|
|
23
|
-
return patternNames(pattern.left);
|
|
24
|
-
case "RestElement":
|
|
25
|
-
return patternNames(pattern.argument);
|
|
26
|
-
default:
|
|
27
|
-
return [];
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
// The names a declaration statement introduces, with what it declares them as.
|
|
31
|
-
const declaredNamesOf = (statement) => {
|
|
32
|
-
const named = (declares) => {
|
|
33
|
-
const name = nameOf(statement.id);
|
|
34
|
-
return name === null ? [] : [[name, declares]];
|
|
35
|
-
};
|
|
36
|
-
switch (statement.type) {
|
|
37
|
-
case "FunctionDeclaration":
|
|
38
|
-
return named("function");
|
|
39
|
-
case "ClassDeclaration":
|
|
40
|
-
return named("class");
|
|
41
|
-
case "VariableDeclaration":
|
|
42
|
-
return (statement.declarations ?? []).flatMap((declaration) => patternNames(declaration.id).map((name) => [name, "variable"]));
|
|
43
|
-
case "TSTypeAliasDeclaration":
|
|
44
|
-
return named("type");
|
|
45
|
-
case "TSInterfaceDeclaration":
|
|
46
|
-
return named("interface");
|
|
47
|
-
case "TSEnumDeclaration":
|
|
48
|
-
return named("enum");
|
|
49
|
-
case "TSModuleDeclaration":
|
|
50
|
-
return named("other");
|
|
51
|
-
default:
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
// A file's surface, read off the program body rather than by visiting export
|
|
56
|
-
// nodes: an `export` inside a namespace body is that namespace's, not the
|
|
57
|
-
// module's, and reading the top level directly is what says so without a
|
|
58
|
-
// parent pointer. Mirrors the CLI's `exportSitesOf`.
|
|
59
|
-
const surfaceOf = (file, body) => {
|
|
60
|
-
const locals = new Map();
|
|
61
|
-
for (const statement of body) {
|
|
62
|
-
const declaration = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration"
|
|
63
|
-
? statement.declaration
|
|
64
|
-
: statement;
|
|
65
|
-
if (declaration === null || declaration === undefined)
|
|
66
|
-
continue;
|
|
67
|
-
for (const [name, declares] of declaredNamesOf(declaration))
|
|
68
|
-
locals.set(name, declares);
|
|
69
|
-
}
|
|
70
|
-
const found = [];
|
|
71
|
-
const site = (node, name, kind, declares, reexport) => {
|
|
72
|
-
found.push({ node, site: { file, name, kind, declares, reexport } });
|
|
73
|
-
};
|
|
74
|
-
for (const statement of body) {
|
|
75
|
-
switch (statement.type) {
|
|
76
|
-
case "ExportNamedDeclaration": {
|
|
77
|
-
const declaration = statement.declaration;
|
|
78
|
-
if (declaration !== null && declaration !== undefined) {
|
|
79
|
-
for (const [name, declares] of declaredNamesOf(declaration)) {
|
|
80
|
-
site(declaration, name, "named", declares, false);
|
|
81
|
-
}
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
const reexport = typeof statement.source?.value === "string";
|
|
85
|
-
for (const specifier of statement.specifiers ?? []) {
|
|
86
|
-
const name = nameOf(specifier.exported);
|
|
87
|
-
if (name === null)
|
|
88
|
-
continue;
|
|
89
|
-
const local = nameOf(specifier.local) ?? name;
|
|
90
|
-
const declares = reexport ? "other" : (locals.get(local) ?? "other");
|
|
91
|
-
site(specifier, name, name === "default" ? "default" : "named", declares, reexport);
|
|
92
|
-
}
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
case "ExportDefaultDeclaration": {
|
|
96
|
-
const declaration = statement.declaration;
|
|
97
|
-
const declares = declaration === null || declaration === undefined
|
|
98
|
-
? "expression"
|
|
99
|
-
: declaration.type === "FunctionDeclaration"
|
|
100
|
-
? "function"
|
|
101
|
-
: declaration.type === "ClassDeclaration"
|
|
102
|
-
? "class"
|
|
103
|
-
: declaration.type === "TSInterfaceDeclaration"
|
|
104
|
-
? "interface"
|
|
105
|
-
: declaration.type === "Identifier"
|
|
106
|
-
? (locals.get(nameOf(declaration) ?? "") ?? "expression")
|
|
107
|
-
: "expression";
|
|
108
|
-
site(statement, "default", "default", declares, false);
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
case "ExportAllDeclaration":
|
|
112
|
-
site(statement, nameOf(statement.exported) ?? "*", "namespace", "other", true);
|
|
113
|
-
break;
|
|
114
|
-
default:
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return found;
|
|
119
|
-
};
|
|
2
|
+
import { at, factsOfProgram, toRepoRelative, } from "./oxlint-api.js";
|
|
120
3
|
export const makeSurfaceRule = (policy) => ({
|
|
121
4
|
meta: {
|
|
122
5
|
type: "problem",
|
|
@@ -137,22 +20,23 @@ export const makeSurfaceRule = (policy) => ({
|
|
|
137
20
|
return selected.length > 0;
|
|
138
21
|
},
|
|
139
22
|
Program(node) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const found = surfaceOf(file, node.body);
|
|
143
|
-
const violations = evaluateSurface(selected, file, found.map((one) => one.site));
|
|
23
|
+
const sites = factsOfProgram(file, node).exportSites;
|
|
24
|
+
const violations = evaluateSurface(selected, file, sites);
|
|
144
25
|
// A per-site violation lands on the site's own node; a `count` one, on
|
|
145
26
|
// the program — it is about the file.
|
|
146
27
|
const used = new Set();
|
|
147
28
|
for (const violation of violations) {
|
|
148
29
|
if (policy.baseline.isBaselined(violation))
|
|
149
30
|
continue;
|
|
150
|
-
const
|
|
31
|
+
const site = violation.subject === null
|
|
151
32
|
? undefined
|
|
152
|
-
:
|
|
153
|
-
if (
|
|
154
|
-
used.add(
|
|
155
|
-
context.report({
|
|
33
|
+
: sites.find((one) => one.name === violation.subject && !used.has(one));
|
|
34
|
+
if (site !== undefined)
|
|
35
|
+
used.add(site);
|
|
36
|
+
context.report({
|
|
37
|
+
node: site === undefined ? node : at(site.node),
|
|
38
|
+
message: formatMessage(violation),
|
|
39
|
+
});
|
|
156
40
|
}
|
|
157
41
|
},
|
|
158
42
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"surface-rule.js","sourceRoot":"","sources":["../../src/surface-rule.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"surface-rule.js","sourceRoot":"","sources":["../../src/surface-rule.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,eAAe,EACf,aAAa,EAEb,qBAAqB,GACtB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,EAAE,EACF,cAAc,EAId,cAAc,GACf,MAAM,iBAAiB,CAAC;AAEzB,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAoB,EAAc,EAAE,CAAC,CAAC;IACpE,IAAI,EAAE;QACJ,IAAI,EAAE,SAAkB;QACxB,IAAI,EAAE;YACJ,WAAW,EACT,iHAAiH;SACpH;QACD,MAAM,EAAE,EAAE;KACX;IAED,UAAU,CAAC,OAAoB;QAC7B,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,IAAI,QAAQ,GAAuC,EAAE,CAAC;QAEtD,OAAO;YACL,MAAM;gBACJ,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;gBACzD,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;oBAAE,OAAO,KAAK,CAAC;gBACxC,QAAQ,GAAG,qBAAqB,CAAC,MAAM,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;gBAC5D,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YAC7B,CAAC;YAED,OAAO,CAAC,IAAa;gBACnB,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,WAAW,CAAC;gBACrD,MAAM,UAAU,GAAG,eAAe,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;gBAE1D,uEAAuE;gBACvE,sCAAsC;gBACtC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;gBACvC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;oBACnC,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,SAAS,CAAC;wBAAE,SAAS;oBACrD,MAAM,IAAI,GACR,SAAS,CAAC,OAAO,KAAK,IAAI;wBACxB,CAAC,CAAC,SAAS;wBACX,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;oBAC5E,IAAI,IAAI,KAAK,SAAS;wBAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;oBACvC,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;wBAC/C,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC;qBAClC,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodbones/oxlint",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "The oxlint plugin for @goodbones/core: the imports, exports, members, structure and surface families as five oxlint rules, evaluated in the editor and in CI from the same manifest the CLI reads.",
|
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"effect": "4.0.0-beta.94",
|
|
47
|
-
"@goodbones/
|
|
48
|
-
"@goodbones/
|
|
47
|
+
"@goodbones/ast-grep": "0.1.2",
|
|
48
|
+
"@goodbones/core": "0.1.0-beta.10",
|
|
49
|
+
"@goodbones/typescript": "0.1.0-beta.10"
|
|
49
50
|
},
|
|
50
51
|
"peerDependencies": {
|
|
51
52
|
"oxlint": ">=1.77.0"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CampaignHit,
|
|
3
|
+
campaignsSelecting,
|
|
4
|
+
type CompiledCampaign,
|
|
5
|
+
evaluateCampaigns,
|
|
6
|
+
formatMessage,
|
|
7
|
+
leafTermsOf,
|
|
8
|
+
type LoadedPolicy,
|
|
9
|
+
reconcile,
|
|
10
|
+
ReportUnavailable,
|
|
11
|
+
} from "@goodbones/core";
|
|
12
|
+
import { sourceFactsOf } from "@goodbones/typescript";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
factsOfProgram,
|
|
16
|
+
type OxlintRule,
|
|
17
|
+
type Program,
|
|
18
|
+
type RuleContext,
|
|
19
|
+
toRepoRelative,
|
|
20
|
+
} from "./oxlint-api.js";
|
|
21
|
+
|
|
22
|
+
// The campaigns family, one file at a time: every hit of a campaign selecting
|
|
23
|
+
// the file that its ledger does not carry, reported at the match's position
|
|
24
|
+
// with the campaign's `how` as the message. The facts come from oxlint's tree
|
|
25
|
+
// through the pack's reader, as the other rules read them; the `syntax` term
|
|
26
|
+
// is answered by the same matcher the CLI uses, over `sourceCode.text` —
|
|
27
|
+
// "one engine" is this family's parity contract, rather than a corpus.
|
|
28
|
+
|
|
29
|
+
// Whether a hit is carried by the ledger, judged against the file's hits
|
|
30
|
+
// alone: entries are per file, so a file's reconciliation is the whole
|
|
31
|
+
// campaign's restricted to it.
|
|
32
|
+
const unledgered = (
|
|
33
|
+
policy: LoadedPolicy,
|
|
34
|
+
selected: ReadonlyArray<CompiledCampaign>,
|
|
35
|
+
hits: ReadonlyArray<CampaignHit>,
|
|
36
|
+
): ReadonlyArray<CampaignHit> => {
|
|
37
|
+
const carried = new Set<CampaignHit["violation"]>();
|
|
38
|
+
for (const rule of selected) {
|
|
39
|
+
const ledger = policy.ledgers.get(rule.id);
|
|
40
|
+
if (ledger === undefined) continue;
|
|
41
|
+
const own = hits.filter((hit) => hit.campaign === rule.id).map((hit) => hit.violation);
|
|
42
|
+
for (const one of reconcile(ledger, own, rule.unit).ledgered) carried.add(one);
|
|
43
|
+
}
|
|
44
|
+
return hits.filter((hit) => !carried.has(hit.violation));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// A `report` a campaign names that cannot be read — a command the kernel
|
|
48
|
+
// refused to spawn, a file no step wrote — is one fact about the run, not
|
|
49
|
+
// one about each file the campaign selects. The live source keeps the
|
|
50
|
+
// failure per spec; this keeps which failures have been said, so the first
|
|
51
|
+
// selected file carries the notice and the rest stay quiet. Without it, a
|
|
52
|
+
// lint of eight hundred files is eight hundred copies of oxlint's generic
|
|
53
|
+
// "error running JS plugin", with the cause dropped by the terser formats.
|
|
54
|
+
const aside = (cause: ReportUnavailable): string =>
|
|
55
|
+
`A report a campaign names could not be read, so no campaign naming it is judged in this ` +
|
|
56
|
+
`run: ${cause.message}`;
|
|
57
|
+
|
|
58
|
+
export const makeCampaignsRule = (policy: LoadedPolicy): OxlintRule => {
|
|
59
|
+
// Per rule instance, which is per plugin load — one process — rather than
|
|
60
|
+
// per `createOnce`, which a host may call more often than that.
|
|
61
|
+
const said = new Set<string>();
|
|
62
|
+
return {
|
|
63
|
+
meta: {
|
|
64
|
+
type: "problem" as const,
|
|
65
|
+
docs: {
|
|
66
|
+
description:
|
|
67
|
+
"the campaigns: every place a pattern the repository is migrating away from still occurs, that its ledger does not carry",
|
|
68
|
+
},
|
|
69
|
+
schema: [],
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
createOnce(context: RuleContext) {
|
|
73
|
+
let file = "";
|
|
74
|
+
let selected: ReadonlyArray<CompiledCampaign> = [];
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
before() {
|
|
78
|
+
file = toRepoRelative(policy.repoRoot, context.filename);
|
|
79
|
+
if (file.startsWith("..")) return false;
|
|
80
|
+
selected = campaignsSelecting(policy.campaignRules, file);
|
|
81
|
+
return selected.length > 0;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
Program(node: Program) {
|
|
85
|
+
const text = context.sourceCode.text;
|
|
86
|
+
const needsSyntax = selected.some((rule) =>
|
|
87
|
+
leafTermsOf(rule.detect).some(
|
|
88
|
+
(leaf) => leaf === "syntax" || leaf === "report" || leaf === "fn",
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
const input = {
|
|
92
|
+
file,
|
|
93
|
+
text,
|
|
94
|
+
facts: sourceFactsOf(factsOfProgram(file, node)),
|
|
95
|
+
resolver: policy.resolver,
|
|
96
|
+
fileSystem: policy.fileSystem,
|
|
97
|
+
syntax: needsSyntax ? policy.syntax.parse(file, text) : null,
|
|
98
|
+
functions: policy.functions,
|
|
99
|
+
reports: policy.reports,
|
|
100
|
+
};
|
|
101
|
+
// One campaign at a time, so a report one of them cannot read costs
|
|
102
|
+
// that campaign's answer for this file and not its neighbours'.
|
|
103
|
+
const hits: Array<CampaignHit> = [];
|
|
104
|
+
for (const rule of selected) {
|
|
105
|
+
try {
|
|
106
|
+
for (const hit of evaluateCampaigns([rule], input)) hits.push(hit);
|
|
107
|
+
} catch (cause) {
|
|
108
|
+
if (!(cause instanceof ReportUnavailable)) throw cause;
|
|
109
|
+
const message = aside(cause);
|
|
110
|
+
if (said.has(message)) continue;
|
|
111
|
+
said.add(message);
|
|
112
|
+
context.report({ message, loc: { line: 1, column: 0 } });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const hit of unledgered(policy, selected, hits)) {
|
|
117
|
+
// A match lands where it was found; a file-unit hit, on line 1, as
|
|
118
|
+
// the structure rule places a finding about the file itself.
|
|
119
|
+
const at = hit.range ?? { start: { line: 0, column: 0 } };
|
|
120
|
+
context.report({
|
|
121
|
+
message: formatMessage(hit.violation),
|
|
122
|
+
loc: { line: at.start.line + 1, column: at.start.column },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
};
|