@goodbones/oxlint 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/build/dts/config-loader.d.ts +5 -0
  3. package/build/dts/config-loader.d.ts.map +1 -0
  4. package/build/dts/exports-rule.d.ts +4 -0
  5. package/build/dts/exports-rule.d.ts.map +1 -0
  6. package/build/dts/imports-rule.d.ts +4 -0
  7. package/build/dts/imports-rule.d.ts.map +1 -0
  8. package/build/dts/members-rule.d.ts +4 -0
  9. package/build/dts/members-rule.d.ts.map +1 -0
  10. package/build/dts/oxlint-api.d.ts +46 -0
  11. package/build/dts/oxlint-api.d.ts.map +1 -0
  12. package/build/dts/plugin.d.ts +16 -0
  13. package/build/dts/plugin.d.ts.map +1 -0
  14. package/build/dts/structure-rule.d.ts +4 -0
  15. package/build/dts/structure-rule.d.ts.map +1 -0
  16. package/build/dts/surface-rule.d.ts +4 -0
  17. package/build/dts/surface-rule.d.ts.map +1 -0
  18. package/build/esm/config-loader.js +24 -0
  19. package/build/esm/config-loader.js.map +1 -0
  20. package/build/esm/exports-rule.js +137 -0
  21. package/build/esm/exports-rule.js.map +1 -0
  22. package/build/esm/imports-rule.js +71 -0
  23. package/build/esm/imports-rule.js.map +1 -0
  24. package/build/esm/members-rule.js +138 -0
  25. package/build/esm/members-rule.js.map +1 -0
  26. package/build/esm/oxlint-api.js +24 -0
  27. package/build/esm/oxlint-api.js.map +1 -0
  28. package/build/esm/plugin.js +30 -0
  29. package/build/esm/plugin.js.map +1 -0
  30. package/build/esm/structure-rule.js +36 -0
  31. package/build/esm/structure-rule.js.map +1 -0
  32. package/build/esm/surface-rule.js +161 -0
  33. package/build/esm/surface-rule.js.map +1 -0
  34. package/package.json +67 -0
  35. package/src/config-loader.ts +35 -0
  36. package/src/exports-rule.ts +195 -0
  37. package/src/imports-rule.ts +96 -0
  38. package/src/members-rule.ts +205 -0
  39. package/src/oxlint-api.ts +66 -0
  40. package/src/plugin.ts +44 -0
  41. package/src/structure-rule.ts +38 -0
  42. package/src/surface-rule.ts +237 -0
@@ -0,0 +1,36 @@
1
+ import { evaluateStructure, formatMessage } from "@goodbones/core";
2
+ import { toRepoRelative } from "./oxlint-api.js";
3
+ export const makeStructureRule = (policy) => ({
4
+ meta: {
5
+ type: "problem",
6
+ docs: {
7
+ description: "the file taxonomy: which file kinds a folder admits, and which siblings a file owes",
8
+ },
9
+ schema: [],
10
+ },
11
+ createOnce(context) {
12
+ // The taxonomy is a property of the path, not of the syntax, so the whole
13
+ // check runs once in `before` and the visitor exists only because oxlint
14
+ // does not run hooks for a rule with no visitor keys.
15
+ return {
16
+ before() {
17
+ const file = toRepoRelative(policy.repoRoot, context.filename);
18
+ if (file.startsWith(".."))
19
+ return false;
20
+ for (const violation of evaluateStructure(policy.structure, policy.fileSystem, file)) {
21
+ if (policy.baseline.isBaselined(violation))
22
+ continue;
23
+ context.report({
24
+ message: formatMessage(violation),
25
+ loc: { line: 1, column: 0 },
26
+ });
27
+ }
28
+ return false;
29
+ },
30
+ Program() {
31
+ return undefined;
32
+ },
33
+ };
34
+ },
35
+ });
36
+ //# sourceMappingURL=structure-rule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structure-rule.js","sourceRoot":"","sources":["../../src/structure-rule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAqB,MAAM,iBAAiB,CAAC;AAEtF,OAAO,EAAqC,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEpF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,MAAoB,EAAc,EAAE,CAAC,CAAC;IACtE,IAAI,EAAE;QACJ,IAAI,EAAE,SAAkB;QACxB,IAAI,EAAE;YACJ,WAAW,EACT,qFAAqF;SACxF;QACD,MAAM,EAAE,EAAE;KACX;IAED,UAAU,CAAC,OAAoB;QAC7B,0EAA0E;QAC1E,yEAAyE;QACzE,sDAAsD;QACtD,OAAO;YACL,MAAM;gBACJ,MAAM,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAC/D,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;oBAAE,OAAO,KAAK,CAAC;gBAExC,KAAK,MAAM,SAAS,IAAI,iBAAiB,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC;oBACrF,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,SAAS,CAAC;wBAAE,SAAS;oBACrD,OAAO,CAAC,MAAM,CAAC;wBACb,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC;wBACjC,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;qBAC5B,CAAC,CAAC;gBACL,CAAC;gBACD,OAAO,KAAK,CAAC;YACf,CAAC;YACD,OAAO;gBACL,OAAO,SAAS,CAAC;YACnB,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,161 @@
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
+ };
120
+ export const makeSurfaceRule = (policy) => ({
121
+ meta: {
122
+ type: "problem",
123
+ docs: {
124
+ description: "what a file may export — no default exports, a barrel that only re-exports, a handler that exports one function",
125
+ },
126
+ schema: [],
127
+ },
128
+ createOnce(context) {
129
+ let file = "";
130
+ let selected = [];
131
+ return {
132
+ before() {
133
+ file = toRepoRelative(policy.repoRoot, context.filename);
134
+ if (file.startsWith(".."))
135
+ return false;
136
+ selected = surfaceRulesSelecting(policy.surfaceRules, file);
137
+ return selected.length > 0;
138
+ },
139
+ Program(node) {
140
+ if (!isStatementList(node.body))
141
+ return;
142
+ const found = surfaceOf(file, node.body);
143
+ const violations = evaluateSurface(selected, file, found.map((one) => one.site));
144
+ // A per-site violation lands on the site's own node; a `count` one, on
145
+ // the program — it is about the file.
146
+ const used = new Set();
147
+ for (const violation of violations) {
148
+ if (policy.baseline.isBaselined(violation))
149
+ continue;
150
+ const at = violation.subject === null
151
+ ? undefined
152
+ : found.find((one) => one.site.name === violation.subject && !used.has(one));
153
+ if (at !== undefined)
154
+ used.add(at);
155
+ context.report({ node: at?.node ?? node, message: formatMessage(violation) });
156
+ }
157
+ },
158
+ };
159
+ },
160
+ });
161
+ //# sourceMappingURL=surface-rule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"surface-rule.js","sourceRoot":"","sources":["../../src/surface-rule.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@goodbones/oxlint",
3
+ "version": "0.1.0-beta.1",
4
+ "type": "module",
5
+ "license": "MIT",
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.",
7
+ "author": "zacharyweidenbach",
8
+ "keywords": [
9
+ "architecture",
10
+ "lint",
11
+ "import-boundaries",
12
+ "hexagonal",
13
+ "monorepo"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/dataquail/goodbones.git",
21
+ "directory": "packages/oxlint"
22
+ },
23
+ "homepage": "https://dataquail.github.io/goodbones",
24
+ "bugs": {
25
+ "url": "https://github.com/dataquail/goodbones/issues"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "types": "./build/dts/plugin.d.ts",
30
+ "default": "./build/esm/plugin.js"
31
+ },
32
+ "./plugin": {
33
+ "types": "./build/dts/plugin.d.ts",
34
+ "default": "./build/esm/plugin.js"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "types": "./build/dts/plugin.d.ts",
39
+ "files": [
40
+ "build/dts",
41
+ "build/esm",
42
+ "src",
43
+ "!src/**/*.test.ts"
44
+ ],
45
+ "dependencies": {
46
+ "effect": "4.0.0-beta.94",
47
+ "@goodbones/core": "0.1.0-beta.1",
48
+ "@goodbones/typescript": "0.1.0-beta.1"
49
+ },
50
+ "peerDependencies": {
51
+ "oxlint": ">=1.77.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^25.9.4",
55
+ "oxlint": "1.81.0"
56
+ },
57
+ "nx": {
58
+ "includedScripts": []
59
+ },
60
+ "scripts": {
61
+ "build": "tsc -b tsconfig.build.json",
62
+ "check": "tsc -b tsconfig.json",
63
+ "clean": "rm -rf build .tsbuildinfo",
64
+ "test": "vitest run",
65
+ "coverage": "vitest run --coverage"
66
+ }
67
+ }
@@ -0,0 +1,35 @@
1
+ import * as path from "node:path";
2
+
3
+ import {
4
+ DEFAULT_CONFIG_FILENAME,
5
+ type LoadedPolicy,
6
+ loadPolicy,
7
+ makeFileSystemLive,
8
+ readManifestFile,
9
+ } from "@goodbones/core";
10
+ import { typescriptLanguage } from "@goodbones/typescript";
11
+ import * as Result from "effect/Result";
12
+
13
+ export type { LoadedPolicy } from "@goodbones/core";
14
+ export { DEFAULT_CONFIG_FILENAME } from "@goodbones/core";
15
+
16
+ // The plugin's composition root: read the manifest file, construct the
17
+ // language packs and the live file system, and hand them to the loader. A load
18
+ // failure throws out of here and out of oxlint's import of the plugin — a
19
+ // plugin that came up with no policy would report nothing and be
20
+ // indistinguishable from a clean codebase.
21
+ export const loadPolicyFromFile = async (
22
+ repoRoot: string,
23
+ configFilename: string = DEFAULT_CONFIG_FILENAME,
24
+ ): Promise<LoadedPolicy> => {
25
+ const configPath = path.resolve(repoRoot, configFilename);
26
+ const loaded = loadPolicy({
27
+ repoRoot,
28
+ configPath,
29
+ manifest: await readManifestFile(configPath),
30
+ languages: [typescriptLanguage()],
31
+ fileSystem: makeFileSystemLive(repoRoot),
32
+ });
33
+ if (Result.isFailure(loaded)) throw loaded.failure;
34
+ return loaded.success;
35
+ };
@@ -0,0 +1,195 @@
1
+ import {
2
+ type Binding,
3
+ evaluateSelectedBindings,
4
+ exportRulesSelecting,
5
+ formatMessage,
6
+ type LoadedPolicy,
7
+ type SelectedExportRule,
8
+ } from "@goodbones/core";
9
+ import * as Result from "effect/Result";
10
+
11
+ import {
12
+ type CallNode,
13
+ type Fixer,
14
+ type ImportEqualsNode,
15
+ importEqualsSpecifierOf,
16
+ type ImportExpressionNode,
17
+ importExpressionSpecifierOf,
18
+ type OxlintRule,
19
+ type ReportableNode,
20
+ requireSpecifierOf,
21
+ type RuleContext,
22
+ toRepoRelative,
23
+ } from "./oxlint-api.js";
24
+
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
+ // `import { A, B as C } from "pkg"` becomes `import * as A from "pkg/A"` and
90
+ // `import * as C from "pkg/B"`. Only whole-declaration rewrites are offered: a
91
+ // declaration mixing restricted named imports with a default or namespace one
92
+ // would need comma surgery inside the braces, and a fix that is subtly wrong is
93
+ // worse than a diagnostic the author resolves by hand.
94
+ const subpathNamespaceImport = (specifier: string, bound: ReadonlyArray<Bound>): string =>
95
+ bound
96
+ .map((binding) => `import * as ${binding.local} from "${specifier}/${binding.symbol}";`)
97
+ .join("\n");
98
+
99
+ export const makeExportsRule = (policy: LoadedPolicy): OxlintRule => ({
100
+ meta: {
101
+ type: "problem" as const,
102
+ fixable: "code" as const,
103
+ docs: {
104
+ description:
105
+ "which exported symbols a file may import, for rules a path alone cannot express",
106
+ },
107
+ schema: [],
108
+ },
109
+
110
+ createOnce(context: RuleContext) {
111
+ let importer = "";
112
+ let selected: ReadonlyArray<SelectedExportRule> = [];
113
+
114
+ // `fixable` is whether a rewrite could apply: only an `import` declaration
115
+ // can be rewritten into subpath namespace imports. `export *` and the
116
+ // whole-module forms are reported and left for the author.
117
+ const check = (
118
+ node: ReportableNode,
119
+ specifierValue: string | null,
120
+ bound: ReadonlyArray<Bound>,
121
+ fixable: boolean,
122
+ ): void => {
123
+ if (specifierValue === null || bound.length === 0) return;
124
+
125
+ const outcome = evaluateSelectedBindings(selected, policy.resolver, {
126
+ importer,
127
+ specifier: specifierValue,
128
+ bindings: bound,
129
+ });
130
+
131
+ if (Result.isFailure(outcome)) {
132
+ // `architecture/imports` reports the same unresolved edge, so staying
133
+ // quiet here avoids two diagnostics for one broken specifier.
134
+ return;
135
+ }
136
+
137
+ for (const { bindings, rule, violation } of outcome.success) {
138
+ if (policy.baseline.isBaselined(violation)) continue;
139
+ const offending = bound.filter((one: Bound) =>
140
+ bindings.some((binding) => binding.symbol === one.symbol && binding.kind === one.kind),
141
+ );
142
+ // The rewrite is `import * as X from "pkg/<name>"`, which only means
143
+ // something for a named binding — a namespace one has no name to put
144
+ // in the subpath.
145
+ const rewritable =
146
+ fixable &&
147
+ rule.fix === "subpath-namespace-import" &&
148
+ offending.length === bound.length &&
149
+ offending.every((one) => one.kind === "named");
150
+
151
+ if (rewritable) {
152
+ context.report({
153
+ node,
154
+ message: formatMessage(violation),
155
+ fix: (fixer: Fixer) =>
156
+ fixer.replaceText(node, subpathNamespaceImport(specifierValue, offending)),
157
+ });
158
+ continue;
159
+ }
160
+
161
+ context.report({
162
+ node: offending[0]?.node ?? node,
163
+ message: formatMessage(violation),
164
+ });
165
+ }
166
+ };
167
+
168
+ return {
169
+ before() {
170
+ importer = toRepoRelative(policy.repoRoot, context.filename);
171
+ if (importer.startsWith("..")) return false;
172
+ selected = exportRulesSelecting(policy.exportRules, importer);
173
+ return selected.length > 0;
174
+ },
175
+ ImportDeclaration(node: DeclarationNode) {
176
+ check(node, specifierOf(node), boundSpecifiers(node), true);
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);
192
+ },
193
+ };
194
+ },
195
+ });
@@ -0,0 +1,96 @@
1
+ import {
2
+ evaluateSelectedEdge,
3
+ formatMessage,
4
+ type LoadedPolicy,
5
+ rulesSelecting,
6
+ type SelectedRule,
7
+ } from "@goodbones/core";
8
+ import * as Result from "effect/Result";
9
+
10
+ import {
11
+ type CallNode,
12
+ type ImportEqualsNode,
13
+ importEqualsSpecifierOf,
14
+ type ImportExpressionNode,
15
+ importExpressionSpecifierOf,
16
+ type OxlintRule,
17
+ type ReportableNode,
18
+ requireSpecifierOf,
19
+ type RuleContext,
20
+ type SourceNode,
21
+ specifierOf,
22
+ toRepoRelative,
23
+ } from "./oxlint-api.js";
24
+
25
+ const unresolvedMessage = (specifier: string, detail: string): string =>
26
+ `[unresolved-import] "${specifier}" could not be resolved, so every import rule about it ` +
27
+ `enforces nothing. Fix the resolve scope in the architecture config, or list the specifier ` +
28
+ `in resolve.ignoreUnresolved. (${detail})`;
29
+
30
+ export const makeImportsRule = (policy: LoadedPolicy): OxlintRule => ({
31
+ meta: {
32
+ type: "problem" as const,
33
+ docs: {
34
+ description:
35
+ "import boundaries between folders and packages, matched against fully resolved module paths",
36
+ },
37
+ schema: [],
38
+ },
39
+
40
+ // `createOnce` is what makes resolution affordable: the resolver and the
41
+ // compiled rules are built once per lint run, and `before` decides per file
42
+ // whether any rule selects it at all — a file none select is skipped whole.
43
+ createOnce(context: RuleContext) {
44
+ let importer = "";
45
+ let selected: ReadonlyArray<SelectedRule> = [];
46
+
47
+ const check = (node: ReportableNode, specifier: string | null): void => {
48
+ if (specifier === null) return;
49
+
50
+ const outcome = evaluateSelectedEdge(selected, policy.resolver, { importer, specifier });
51
+
52
+ if (Result.isFailure(outcome)) {
53
+ if (policy.config.resolve.unresolved === "off") return;
54
+ if (policy.ignoreUnresolved.some((pattern) => pattern.test(specifier))) return;
55
+ context.report({ node, message: unresolvedMessage(specifier, outcome.failure.detail) });
56
+ return;
57
+ }
58
+
59
+ for (const violation of outcome.success) {
60
+ if (policy.baseline.isBaselined(violation)) continue;
61
+ context.report({ node, message: formatMessage(violation) });
62
+ }
63
+ };
64
+
65
+ const checkSource = (node: SourceNode): void => {
66
+ check(node, specifierOf(node));
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`.
73
+ return {
74
+ before() {
75
+ importer = toRepoRelative(policy.repoRoot, context.filename);
76
+ if (importer.startsWith("..")) return false;
77
+ selected = rulesSelecting(policy.importRules, importer);
78
+ return selected.length > 0;
79
+ },
80
+ ImportDeclaration: checkSource,
81
+ ExportNamedDeclaration: checkSource,
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));
93
+ },
94
+ };
95
+ },
96
+ });