@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,205 @@
1
+ import {
2
+ type CompiledMemberRule,
3
+ type DeclarationKind,
4
+ evaluateMemberSite,
5
+ formatMessage,
6
+ type LoadedPolicy,
7
+ memberRulesSelecting,
8
+ } from "@goodbones/core";
9
+
10
+ import {
11
+ type OxlintRule,
12
+ type ReportableNode,
13
+ type RuleContext,
14
+ toRepoRelative,
15
+ } from "./oxlint-api.js";
16
+
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
+ export const makeMembersRule = (policy: LoadedPolicy): OxlintRule => ({
118
+ meta: {
119
+ type: "problem" as const,
120
+ docs: {
121
+ description:
122
+ "which names a file may declare or call — a port's method vocabulary, a tier's allowed hooks",
123
+ },
124
+ schema: [],
125
+ },
126
+
127
+ createOnce(context: RuleContext) {
128
+ let file = "";
129
+ let selected: ReadonlyArray<CompiledMemberRule> = [];
130
+
131
+ const report = (
132
+ node: ReportableNode,
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
+
167
+ return {
168
+ before() {
169
+ file = toRepoRelative(policy.repoRoot, context.filename);
170
+ if (file.startsWith("..")) return false;
171
+ selected = memberRulesSelecting(policy.memberRules, file);
172
+ return selected.length > 0;
173
+ },
174
+
175
+ TSTypeAliasDeclaration(node: TypeAliasNode) {
176
+ const declaration = nameOf(node.id);
177
+ if (declaration === null) return;
178
+ for (const literal of literalsOf(node.typeAnnotation)) {
179
+ declared(declaration, "type", literal.members ?? []);
180
+ }
181
+ },
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
+ };
204
+ },
205
+ });
@@ -0,0 +1,66 @@
1
+ import * as path from "node:path";
2
+
3
+ import type { RuleTester } from "oxlint/plugins-dev";
4
+
5
+ // oxlint publishes no plugin types, so the exact `Context`, node and fixer shapes
6
+ // are recovered from `RuleTester`'s own signature — the one public surface that
7
+ // names them. Hand-rolling them drifts silently against an alpha API.
8
+ export type OxlintRule = Parameters<RuleTester["run"]>[1];
9
+ export type RuleContext = Parameters<Extract<OxlintRule, { createOnce: unknown }>["createOnce"]>[0];
10
+
11
+ type Diagnostic = Parameters<RuleContext["report"]>[0];
12
+ export type ReportableNode = Extract<Diagnostic, { node: unknown }>["node"];
13
+ export type Fixer = Parameters<NonNullable<Diagnostic["fix"]>>[0];
14
+
15
+ export type SourceNode = ReportableNode & {
16
+ readonly source?: { readonly value?: unknown } | null;
17
+ };
18
+
19
+ export const toRepoRelative = (repoRoot: string, filename: string): string =>
20
+ path.relative(repoRoot, filename).replaceAll(path.sep, "/");
21
+
22
+ export const specifierOf = (node: SourceNode): string | null => {
23
+ const value = node.source?.value;
24
+ return typeof value === "string" ? value : null;
25
+ };
26
+
27
+ // The three forms that name a module without an `import … from`. Each is an
28
+ // edge for the import rule and a whole-module binding for the export rule, so
29
+ // both read them through these.
30
+
31
+ // `require("m")` — a CallExpression whose callee is the bare identifier.
32
+ export type CallNode = ReportableNode & {
33
+ readonly callee?: { readonly type: string; readonly name?: unknown } | null;
34
+ readonly arguments?: ReadonlyArray<{ readonly type: string; readonly value?: unknown }> | null;
35
+ };
36
+
37
+ // `import("m")` — `source` is any expression; only a string literal is an edge.
38
+ export type ImportExpressionNode = ReportableNode & {
39
+ readonly source?: { readonly type?: string; readonly value?: unknown } | null;
40
+ };
41
+
42
+ // `import x = require("m")` — the module is under `moduleReference`, not `source`.
43
+ export type ImportEqualsNode = ReportableNode & {
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;
66
+ };
package/src/plugin.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { DEFAULT_CONFIG_FILENAME, loadPolicyFromFile } from "./config-loader.js";
2
+ import { makeExportsRule } from "./exports-rule.js";
3
+ import { makeImportsRule } from "./imports-rule.js";
4
+ import { makeMembersRule } from "./members-rule.js";
5
+ import type { OxlintRule } from "./oxlint-api.js";
6
+ import { makeStructureRule } from "./structure-rule.js";
7
+ import { makeSurfaceRule } from "./surface-rule.js";
8
+
9
+ // oxlint imports this module once per lint run and reads `rules` synchronously,
10
+ // so the policy is loaded here rather than inside a rule. A load failure throws
11
+ // out of the import and fails the run — which is the point: a plugin that came
12
+ // up with no policy would report nothing and be indistinguishable from a clean
13
+ // codebase.
14
+ const policy = await loadPolicyFromFile(
15
+ process.env.ARCHITECTURE_ROOT ?? process.cwd(),
16
+ process.env.ARCHITECTURE_CONFIG ?? DEFAULT_CONFIG_FILENAME,
17
+ );
18
+
19
+ // A manifest written in a shape on its way out still loads; it says so once,
20
+ // here, rather than on every file.
21
+ for (const notice of policy.notices) {
22
+ process.stderr.write(`[architecture] deprecated: ${notice}\n`);
23
+ }
24
+
25
+ export const rules: {
26
+ readonly imports: OxlintRule;
27
+ readonly exports: OxlintRule;
28
+ readonly members: OxlintRule;
29
+ readonly structure: OxlintRule;
30
+ readonly surface: OxlintRule;
31
+ } = {
32
+ imports: makeImportsRule(policy),
33
+ exports: makeExportsRule(policy),
34
+ members: makeMembersRule(policy),
35
+ structure: makeStructureRule(policy),
36
+ surface: makeSurfaceRule(policy),
37
+ };
38
+
39
+ const plugin: { readonly meta: { readonly name: string }; readonly rules: typeof rules } = {
40
+ meta: { name: "architecture" },
41
+ rules,
42
+ };
43
+
44
+ export default plugin;
@@ -0,0 +1,38 @@
1
+ import { evaluateStructure, formatMessage, type LoadedPolicy } from "@goodbones/core";
2
+
3
+ import { type OxlintRule, type RuleContext, toRepoRelative } from "./oxlint-api.js";
4
+
5
+ export const makeStructureRule = (policy: LoadedPolicy): OxlintRule => ({
6
+ meta: {
7
+ type: "problem" as const,
8
+ docs: {
9
+ description:
10
+ "the file taxonomy: which file kinds a folder admits, and which siblings a file owes",
11
+ },
12
+ schema: [],
13
+ },
14
+
15
+ createOnce(context: RuleContext) {
16
+ // The taxonomy is a property of the path, not of the syntax, so the whole
17
+ // check runs once in `before` and the visitor exists only because oxlint
18
+ // does not run hooks for a rule with no visitor keys.
19
+ return {
20
+ before() {
21
+ const file = toRepoRelative(policy.repoRoot, context.filename);
22
+ if (file.startsWith("..")) return false;
23
+
24
+ for (const violation of evaluateStructure(policy.structure, policy.fileSystem, file)) {
25
+ if (policy.baseline.isBaselined(violation)) continue;
26
+ context.report({
27
+ message: formatMessage(violation),
28
+ loc: { line: 1, column: 0 },
29
+ });
30
+ }
31
+ return false;
32
+ },
33
+ Program() {
34
+ return undefined;
35
+ },
36
+ };
37
+ },
38
+ });
@@ -0,0 +1,237 @@
1
+ import {
2
+ type CompiledSurfaceRule,
3
+ type DeclarationKind,
4
+ evaluateSurface,
5
+ type ExportSite,
6
+ formatMessage,
7
+ type LoadedPolicy,
8
+ surfaceRulesSelecting,
9
+ } from "@goodbones/core";
10
+
11
+ import {
12
+ type OxlintRule,
13
+ type ReportableNode,
14
+ type RuleContext,
15
+ toRepoRelative,
16
+ } from "./oxlint-api.js";
17
+
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
+ export const makeSurfaceRule = (policy: LoadedPolicy): OxlintRule => ({
192
+ meta: {
193
+ type: "problem" as const,
194
+ docs: {
195
+ description:
196
+ "what a file may export — no default exports, a barrel that only re-exports, a handler that exports one function",
197
+ },
198
+ schema: [],
199
+ },
200
+
201
+ createOnce(context: RuleContext) {
202
+ let file = "";
203
+ let selected: ReadonlyArray<CompiledSurfaceRule> = [];
204
+
205
+ return {
206
+ before() {
207
+ file = toRepoRelative(policy.repoRoot, context.filename);
208
+ if (file.startsWith("..")) return false;
209
+ selected = surfaceRulesSelecting(policy.surfaceRules, file);
210
+ return selected.length > 0;
211
+ },
212
+
213
+ Program(node: ProgramNode) {
214
+ if (!isStatementList(node.body)) return;
215
+ const found = surfaceOf(file, node.body);
216
+ const violations = evaluateSurface(
217
+ selected,
218
+ file,
219
+ found.map((one) => one.site),
220
+ );
221
+
222
+ // A per-site violation lands on the site's own node; a `count` one, on
223
+ // the program — it is about the file.
224
+ const used = new Set<Found>();
225
+ for (const violation of violations) {
226
+ if (policy.baseline.isBaselined(violation)) continue;
227
+ const at =
228
+ violation.subject === null
229
+ ? undefined
230
+ : found.find((one) => one.site.name === violation.subject && !used.has(one));
231
+ if (at !== undefined) used.add(at);
232
+ context.report({ node: at?.node ?? node, message: formatMessage(violation) });
233
+ }
234
+ },
235
+ };
236
+ },
237
+ });