@starci/eslint-grammar-core 0.13.0 → 0.14.0

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 (73) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +4 -1
  3. package/dist/app-shell.d.ts +15 -0
  4. package/dist/app-shell.js +224 -0
  5. package/dist/chip-label.d.ts +14 -0
  6. package/dist/chip-label.js +117 -0
  7. package/dist/drawing.d.ts +8 -0
  8. package/dist/drawing.js +59 -0
  9. package/dist/grammar-boundary.d.ts +12 -0
  10. package/dist/grammar-boundary.js +88 -0
  11. package/dist/icon.d.ts +27 -0
  12. package/dist/icon.js +293 -0
  13. package/dist/index.d.ts +45 -0
  14. package/dist/index.js +73 -0
  15. package/dist/lib/app-shell-cluster.d.ts +6 -0
  16. package/dist/lib/app-shell-cluster.js +75 -0
  17. package/dist/lib/app-shell-create.d.ts +5 -0
  18. package/dist/lib/app-shell-create.js +37 -0
  19. package/dist/lib/app-shell-owner-source.d.ts +11 -0
  20. package/dist/lib/app-shell-owner-source.js +159 -0
  21. package/dist/lib/app-shell-owner.d.ts +7 -0
  22. package/dist/lib/app-shell-owner.js +97 -0
  23. package/dist/lib/app-shell-scope.d.ts +13 -0
  24. package/dist/lib/app-shell-scope.js +93 -0
  25. package/dist/lib/app-shell-visible.d.ts +13 -0
  26. package/dist/lib/app-shell-visible.js +117 -0
  27. package/dist/lib/app-shell.d.ts +27 -0
  28. package/dist/lib/app-shell.js +185 -0
  29. package/dist/lib/rule-contract.d.ts +12 -0
  30. package/dist/lib/rule-contract.js +23 -0
  31. package/dist/lib/status-tones.generated.d.ts +2 -0
  32. package/dist/lib/status-tones.generated.js +8 -0
  33. package/dist/lib/types.d.ts +113 -0
  34. package/dist/lib/types.js +2 -0
  35. package/dist/lib/why.d.ts +8 -0
  36. package/dist/lib/why.js +42 -0
  37. package/dist/menu-entries.d.ts +27 -0
  38. package/dist/menu-entries.js +142 -0
  39. package/dist/status-colors.d.ts +13 -0
  40. package/dist/status-colors.js +143 -0
  41. package/dist/tokens.d.ts +11 -0
  42. package/dist/tokens.js +166 -0
  43. package/dist/typography.d.ts +13 -0
  44. package/dist/typography.js +72 -0
  45. package/dist/vendor-boundary.d.ts +5 -0
  46. package/dist/vendor-boundary.js +25 -0
  47. package/docs/chip-label.md +30 -0
  48. package/docs/grammar-boundary.md +1 -1
  49. package/docs/status-colors.md +2 -2
  50. package/package.json +18 -15
  51. package/runtime/knowledge/hfs/canon-pins.yaml +11 -10
  52. package/runtime/knowledge/hfs/rules.yaml +23 -0
  53. package/app-shell.mjs +0 -208
  54. package/drawing.mjs +0 -60
  55. package/grammar-boundary.mjs +0 -94
  56. package/icon.mjs +0 -313
  57. package/index.d.ts +0 -28
  58. package/index.mjs +0 -78
  59. package/lib/app-shell-cluster.mjs +0 -66
  60. package/lib/app-shell-create.mjs +0 -33
  61. package/lib/app-shell-owner-source.mjs +0 -124
  62. package/lib/app-shell-owner.mjs +0 -83
  63. package/lib/app-shell-scope.mjs +0 -79
  64. package/lib/app-shell-visible.mjs +0 -92
  65. package/lib/app-shell.mjs +0 -148
  66. package/lib/rule-contract.mjs +0 -11
  67. package/lib/status-tones.generated.mjs +0 -9
  68. package/lib/why.mjs +0 -38
  69. package/menu-entries.mjs +0 -146
  70. package/status-colors.mjs +0 -140
  71. package/tokens.mjs +0 -164
  72. package/typography.mjs +0 -81
  73. package/vendor-boundary.mjs +0 -29
@@ -0,0 +1,159 @@
1
+ /** Resolve only an imported header's owned source and selected public export. */
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import ts from "typescript";
6
+ import parser from "@typescript-eslint/parser";
7
+ import { SourceCode } from "eslint";
8
+ import { importOf, variableOf } from "@starci/eslint-canon-fe/lib/bindings.mjs";
9
+ import { resolveImport } from "@starci/eslint-canon-fe/lib/scope.mjs";
10
+ import { hfsOf } from "@starci/eslint-canon-fe/lib/hfs.mjs";
11
+ import { walk } from "@starci/eslint-canon-fe/lib/ast.mjs";
12
+ import { loadArchitectureConfig } from "../../runtime/scripts/hfs/architecture/config.mjs";
13
+ import { workspaceSourceEntry } from "../../runtime/scripts/hfs/architecture/typescript.mjs";
14
+ import { openHfs } from "../../runtime/scripts/hfs/slots.mjs";
15
+ const RUNTIME = fileURLToPath(new URL("../../runtime/", import.meta.url));
16
+ const roots = new WeakMap();
17
+ const origins = new WeakMap();
18
+ const stateOf = (context) => {
19
+ const root = hfsOf(context).repoRoot;
20
+ const snapshot = origins.get(context.sourceCode) ?? context.sourceCode;
21
+ let state = roots.get(snapshot);
22
+ if (state === undefined) {
23
+ state = { root, files: new Map(), proofs: new Map(), workspaces: null };
24
+ roots.set(snapshot, state);
25
+ }
26
+ return state;
27
+ };
28
+ /** A workspace package from the text of its `package.json`. */
29
+ const workspaceOf = (root, text) => {
30
+ const manifest = JSON.parse(text);
31
+ const record = typeof manifest === "object" && manifest !== null ? manifest : {};
32
+ return {
33
+ root,
34
+ workspace: true,
35
+ name: "name" in record && typeof record.name === "string" ? record.name : null,
36
+ manifest,
37
+ exports: "exports" in record ? record.exports : undefined,
38
+ };
39
+ };
40
+ const workspaceFile = (context, source) => {
41
+ const state = stateOf(context);
42
+ if (!state.workspaces) {
43
+ const hfs = openHfs({ root: RUNTIME, repoRoot: state.root });
44
+ const config = loadArchitectureConfig(state.root, { hfs });
45
+ state.workspaces = config.workspaces.map((relative) => {
46
+ const root = path.join(state.root, relative);
47
+ return workspaceOf(root, fs.readFileSync(path.join(root, "package.json"), "utf8"));
48
+ });
49
+ }
50
+ const workspace = state.workspaces.find((entry) => entry.name && (source === entry.name || source.startsWith(`${entry.name}/`)));
51
+ return workspace ? workspaceSourceEntry(ts, workspace, source) : null;
52
+ };
53
+ const sourceFile = (context, source) => {
54
+ const target = resolveImport(context, source) ?? workspaceFile(context, source);
55
+ if (!target)
56
+ return null;
57
+ const hfs = hfsOf(context);
58
+ return [target, `${target}.tsx`, `${target}.ts`, `${target}/index.tsx`, `${target}/index.ts`]
59
+ .find((file) => /\.[cm]?[jt]sx?$/i.test(file) && !/\.d\.[cm]?ts$/i.test(file) && ts.sys.fileExists(file) && hfs.classify(file).status === "owned") ?? null;
60
+ };
61
+ /** Record the parent of a child node: the parser leaves it to ESLint's traversal, which this independent parse bypasses. */
62
+ const adopt = (child, parent) => {
63
+ if (typeof child === "object" && child !== null && "type" in child && child.type)
64
+ Reflect.set(child, "parent", parent);
65
+ };
66
+ const sourceContext = (context, filename) => {
67
+ const state = stateOf(context);
68
+ const known = state.files.get(filename);
69
+ if (known)
70
+ return known;
71
+ const text = fs.readFileSync(filename, "utf8");
72
+ const parsed = parser.parseForESLint(text, { filePath: filename, sourceType: "module", ecmaFeatures: { jsx: true }, range: true, loc: true, tokens: true, comment: true });
73
+ const sourceCode = new SourceCode({
74
+ text,
75
+ ast: parsed.ast,
76
+ scopeManager: parsed.scopeManager,
77
+ visitorKeys: parsed.visitorKeys,
78
+ parserServices: parsed.services,
79
+ });
80
+ const view = sourceCode;
81
+ walk(view, parsed.ast, (node) => {
82
+ for (const key of view.visitorKeys[node.type] ?? []) {
83
+ const value = Reflect.get(node, key);
84
+ const values = Array.isArray(value) ? value : [value];
85
+ for (const child of values)
86
+ adopt(child, node);
87
+ }
88
+ });
89
+ const owned = { filename, sourceCode: view, settings: context.settings, program: parsed.ast };
90
+ origins.set(view, origins.get(context.sourceCode) ?? context.sourceCode);
91
+ state.files.set(filename, owned);
92
+ return owned;
93
+ };
94
+ const moduleContext = (context, source) => {
95
+ const filename = sourceFile(context, source);
96
+ return filename ? sourceContext(context, filename) : null;
97
+ };
98
+ const namedDeclaration = (declaration, name) => {
99
+ if (declaration?.type === "FunctionDeclaration")
100
+ return declaration.id?.name === name ? declaration : null;
101
+ if (declaration?.type !== "VariableDeclaration")
102
+ return null;
103
+ return declaration.declarations.find((entry) => entry.id.type === "Identifier" && entry.id.name === name)?.init ?? null;
104
+ };
105
+ /** The name an export specifier spells: an identifier's name or a string literal's value. */
106
+ const specifierName = (name) => (name.type === "Identifier" ? name.name : name.value);
107
+ const namedExport = (context, statement, name, visited) => {
108
+ const value = namedDeclaration(statement.declaration, name);
109
+ if (value)
110
+ return { matched: true, owner: { context, value } };
111
+ const specifier = statement.specifiers.find((entry) => specifierName(entry.exported) === name);
112
+ if (!specifier)
113
+ return { matched: false };
114
+ if (!statement.source)
115
+ return { matched: true, owner: { context, value: specifier.local } };
116
+ const target = moduleContext(context, String(statement.source.value));
117
+ return { matched: true, owner: target ? headerExport(target, specifierName(specifier.local), visited) : null };
118
+ };
119
+ /** The selected exported value, following public barrels without scanning unrelated imported implementations. */
120
+ const headerExport = (context, name, seen = new Set()) => {
121
+ const key = `${context.filename}:${name}`;
122
+ if (seen.has(key))
123
+ return null;
124
+ const visited = new Set(seen).add(key);
125
+ for (const statement of context.program.body) {
126
+ if (statement.type === "ExportDefaultDeclaration" && name === "default")
127
+ return { context, value: statement.declaration };
128
+ if (statement.type === "ExportNamedDeclaration") {
129
+ const found = namedExport(context, statement, name, visited);
130
+ if (found.matched)
131
+ return found.owner;
132
+ }
133
+ if (statement.type === "ExportAllDeclaration") {
134
+ const target = moduleContext(context, String(statement.source.value));
135
+ const found = target && headerExport(target, name, visited);
136
+ if (found)
137
+ return found;
138
+ }
139
+ }
140
+ return null;
141
+ };
142
+ /** An imported JSX owner, or null when its implementation cannot be proved from owned source. */
143
+ export const importedHeader = (context, opening) => {
144
+ const name = opening.name;
145
+ const identifier = name.type === "JSXIdentifier" ? name : name.type === "JSXMemberExpression" ? name.object : null;
146
+ if (identifier?.type !== "JSXIdentifier")
147
+ return null;
148
+ const imported = importOf(context, identifier);
149
+ if (!imported)
150
+ return null;
151
+ const definition = variableOf(context, identifier)?.defs.find((entry) => entry.type === "ImportBinding");
152
+ if (name.type === "JSXMemberExpression" && definition?.node.type !== "ImportNamespaceSpecifier")
153
+ return null;
154
+ const target = moduleContext(context, imported.source);
155
+ const exported = name.type === "JSXMemberExpression" ? name.property.name : imported.imported ?? "default";
156
+ return target ? headerExport(target, exported) : null;
157
+ };
158
+ /** The per-repository proof cache shares the same lifetime as parsed header source. */
159
+ export const headerProofs = (context) => stateOf(context).proofs;
@@ -0,0 +1,7 @@
1
+ /** Imported shell-band delegation requires its canonical composition in every visible source alternative. */
2
+ import type { TSESTree } from "@typescript-eslint/types";
3
+ import type { ReaderContext, Resolved } from "./types.ts";
4
+ /** True only for an imported JSX header whose owned implementation reaches a canonical Grammar bar. */
5
+ export declare const provesImportedBar: (context: ReaderContext, opening: TSESTree.JSXOpeningElement) => boolean | null;
6
+ /** True only when every visible compact-header alternative reaches a left Drawer with a drawer Sidebar. */
7
+ export declare const provesNavigationOpener: (context: ReaderContext, expression: Resolved | null | undefined) => boolean;
@@ -0,0 +1,97 @@
1
+ import { functionOf, importOf } from "@starci/eslint-canon-fe/lib/bindings.mjs";
2
+ import { isBarControl, propOf, realNode, resolveLocal, textProp } from "./app-shell.js";
3
+ import { isNavigationDrawer } from "./app-shell-scope.js";
4
+ import { visitFunction, visitVisible } from "./app-shell-visible.js";
5
+ import { headerProofs, importedHeader } from "./app-shell-owner-source.js";
6
+ const OVERLAYS = new Set(["Dialog", "AlertDialog", "Drawer", "Popover", "DropdownMenu", "Tooltip"]);
7
+ const importBinding = (context, opening) => {
8
+ const name = opening.name.type === "JSXMemberExpression" ? opening.name.object : opening.name;
9
+ return name.type === "JSXIdentifier" ? importOf(context, name) : null;
10
+ };
11
+ const reactFragment = (context, opening) => {
12
+ const imported = importBinding(context, opening);
13
+ return imported?.source === "react" && (imported.imported === "Fragment" || opening.name.type === "JSXMemberExpression" && opening.name.property.name === "Fragment");
14
+ };
15
+ const proveOwner = (owner, seen, kind) => {
16
+ if (!owner)
17
+ return false;
18
+ const { context, value } = owner;
19
+ const key = `${kind}:${context.filename}:${value.range.join(":")}`;
20
+ const proofs = headerProofs(context);
21
+ const known = proofs.get(key);
22
+ if (known !== undefined)
23
+ return known;
24
+ if (seen.has(key))
25
+ return false;
26
+ const fn = functionOf(context, realNode(resolveLocal(context, value)));
27
+ const valid = fn !== null && proveComposition(context, (visit, branch) => visitFunction(context, fn, visit, new Set(), branch, []), new Set(seen).add(key), kind);
28
+ proofs.set(key, valid);
29
+ return valid;
30
+ };
31
+ /** Drawer bodies must expose navigation on every branch, outside nested overlay content. */
32
+ const proveDrawerBody = (context, opening, seen) => proveComposition(context, (visit, branch) => {
33
+ const children = opening.parent.children.filter((child) => child.type !== "JSXText" || child.value.trim());
34
+ if (children.length)
35
+ for (const child of children)
36
+ visitVisible(context, child, visit, new Set(), branch);
37
+ else
38
+ visitVisible(context, propOf(context, opening, "children").value, visit, new Set(), branch);
39
+ }, seen, "sidebar");
40
+ const matchesComposition = (context, opening, name, seen, kind) => {
41
+ if (kind === "drawer")
42
+ return isNavigationDrawer(context, opening) && proveDrawerBody(context, opening, seen);
43
+ if (kind === "sidebar")
44
+ return name === "Sidebar" && textProp(context, opening, "presentation") === "drawer";
45
+ return name !== null && ["TopBar", "NavigationFeatureNav"].includes(name);
46
+ };
47
+ const proveComposition = (context, render, seen, kind) => {
48
+ const pending = [new Map()];
49
+ for (let choices = pending.pop(); choices; choices = pending.pop()) {
50
+ const current = choices;
51
+ const branch = [];
52
+ let found = false;
53
+ let unknown = false;
54
+ render((opening, name) => {
55
+ if (reactFragment(context, opening))
56
+ return;
57
+ if (matchesComposition(context, opening, name, seen, kind)) {
58
+ found = true;
59
+ return false;
60
+ }
61
+ if (name !== null && OVERLAYS.has(name))
62
+ return false;
63
+ if (kind === "bar" && (name !== null && ["header", "nav", "main"].includes(name) || isBarControl(context, opening, name))) {
64
+ unknown = true;
65
+ return false;
66
+ }
67
+ if (name === null && importBinding(context, opening)) {
68
+ if (proveOwner(importedHeader(context, opening), seen, kind))
69
+ found = true;
70
+ else
71
+ unknown = true;
72
+ return false;
73
+ }
74
+ return undefined;
75
+ }, (node) => {
76
+ if (!current.has(node))
77
+ branch.push(node);
78
+ return current.get(node) ?? "consequent";
79
+ });
80
+ const missing = branch.find((node) => !current.has(node));
81
+ if (missing)
82
+ pending.push(new Map([...choices, [missing, "alternate"]]), new Map([...choices, [missing, "consequent"]]));
83
+ else if (!found || unknown)
84
+ return false;
85
+ }
86
+ return true;
87
+ };
88
+ /** True only for an imported JSX header whose owned implementation reaches a canonical Grammar bar. */
89
+ export const provesImportedBar = (context, opening) => {
90
+ if (reactFragment(context, opening))
91
+ return null;
92
+ if (!importBinding(context, opening))
93
+ return null;
94
+ return proveOwner(importedHeader(context, opening), new Set(), "bar");
95
+ };
96
+ /** True only when every visible compact-header alternative reaches a left Drawer with a drawer Sidebar. */
97
+ export const provesNavigationOpener = (context, expression) => proveComposition(context, (visit, branch) => visitVisible(context, expression, visit, new Set(), branch), new Set(), "drawer");
@@ -0,0 +1,13 @@
1
+ /** Structural app-shell ownership, distinct from generic flow-based site chrome. */
2
+ import type { TSESTree } from "@typescript-eslint/types";
3
+ import type { ReaderContext } from "./types.ts";
4
+ /** A workspace identity or a sidebar/navigation slot identifies an application shell structurally. */
5
+ export declare const isAppShell: (context: ReaderContext, opening: TSESTree.JSXOpeningElement) => boolean;
6
+ /** A left Drawer containing a drawer Sidebar is the mobile navigation opener, including an extracted client owner. */
7
+ export declare const isNavigationDrawer: (context: ReaderContext, opening: TSESTree.JSXOpeningElement) => boolean;
8
+ /** Shell-named React owners and the HFS layout kind compose an app frame, rather than page-local navigation. */
9
+ export declare const ownsAppFrame: (context: ReaderContext, node: TSESTree.JSXOpeningElement) => boolean;
10
+ /** Every visible opening hosted by a structurally identified app shell in this source file. */
11
+ export declare const appBarOpenings: (context: ReaderContext) => Set<TSESTree.JSXOpeningElement>;
12
+ /** Grammar bars are app bars unless an explicit generic flow shell establishes site-chrome ownership. */
13
+ export declare const isAppBar: (context: ReaderContext, opening: TSESTree.JSXOpeningElement) => boolean;
@@ -0,0 +1,93 @@
1
+ import { walk } from "@starci/eslint-canon-fe/lib/ast.mjs";
2
+ import { kindOfFile, roleOfFile } from "@starci/eslint-canon-fe/lib/scope.mjs";
3
+ import { grammarName, propOf, textProp } from "./app-shell.js";
4
+ import { visitVisible } from "./app-shell-visible.js";
5
+ /** A workspace identity or a sidebar/navigation slot identifies an application shell structurally. */
6
+ export const isAppShell = (context, opening) => {
7
+ if (grammarName(context, opening) !== "WorkspaceShell")
8
+ return false;
9
+ const viewport = textProp(context, opening, "viewport");
10
+ if (viewport === "fixed" || viewport !== "flow" && ownsAppFrame(context, opening))
11
+ return true;
12
+ const navigation = propOf(context, opening, "navigation");
13
+ if (navigation.value !== null || !navigation.known)
14
+ return true;
15
+ let identity = false;
16
+ visitVisible(context, propOf(context, opening, "header").value, (_node, name) => {
17
+ if (name === "IdentityLockup")
18
+ identity = true;
19
+ });
20
+ return identity;
21
+ };
22
+ /** A left Drawer containing a drawer Sidebar is the mobile navigation opener, including an extracted client owner. */
23
+ export const isNavigationDrawer = (context, opening) => {
24
+ if (grammarName(context, opening) !== "Drawer" || textProp(context, opening, "placement") !== "left")
25
+ return false;
26
+ let sidebar = false;
27
+ const inspect = (node, name) => {
28
+ if (name === "Sidebar" && textProp(context, node, "presentation") === "drawer")
29
+ sidebar = true;
30
+ };
31
+ for (const child of opening.parent.children)
32
+ visitVisible(context, child, inspect);
33
+ visitVisible(context, propOf(context, opening, "children").value, inspect);
34
+ return sidebar;
35
+ };
36
+ /** Shell-named React owners and the HFS layout kind compose an app frame, rather than page-local navigation. */
37
+ export const ownsAppFrame = (context, node) => {
38
+ if (kindOfFile(context) === "layouts" || roleOfFile(context) === "layout")
39
+ return true;
40
+ let rendered = null;
41
+ let shellName = false;
42
+ for (let current = node.parent; current; current = current.parent) {
43
+ if (current.type === "ReturnStatement")
44
+ rendered = current.argument;
45
+ if (current.type === "FunctionDeclaration") {
46
+ shellName = /Shell(?:Base)?$/.test(current.id?.name ?? "");
47
+ break;
48
+ }
49
+ if (current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") {
50
+ rendered ??= current.body;
51
+ shellName = current.parent?.type === "VariableDeclarator" && /Shell(?:Base)?$/.test(current.parent.id.type === "Identifier" ? current.parent.id.name : "");
52
+ break;
53
+ }
54
+ }
55
+ if (shellName)
56
+ return true;
57
+ let sidebar = false;
58
+ visitVisible(context, rendered, (_opening, name) => { if (name === "Sidebar")
59
+ sidebar = true; });
60
+ return sidebar;
61
+ };
62
+ const APP_BAR_CACHE = new WeakMap();
63
+ /** Every visible opening hosted by a structurally identified app shell in this source file. */
64
+ export const appBarOpenings = (context) => {
65
+ const source = context.sourceCode;
66
+ const cached = APP_BAR_CACHE.get(source);
67
+ if (cached)
68
+ return cached;
69
+ const openings = new Set();
70
+ walk(source, source.ast, (node) => {
71
+ if (node.type !== "JSXOpeningElement" || !isAppShell(context, node))
72
+ return;
73
+ for (const slot of ["header", "compactHeader"]) {
74
+ visitVisible(context, propOf(context, node, slot).value, (opening) => { openings.add(opening); });
75
+ }
76
+ });
77
+ APP_BAR_CACHE.set(source, openings);
78
+ return openings;
79
+ };
80
+ /** Grammar bars are app bars unless an explicit generic flow shell establishes site-chrome ownership. */
81
+ export const isAppBar = (context, opening) => {
82
+ const barName = grammarName(context, opening);
83
+ if (barName !== "TopBar" && barName !== "NavigationFeatureNav")
84
+ return false;
85
+ for (let current = opening.parent; current; current = current.parent) {
86
+ if (current.type !== "JSXElement")
87
+ continue;
88
+ const shell = current.openingElement;
89
+ if (grammarName(context, shell) === "WorkspaceShell" && textProp(context, shell, "viewport") === "flow" && !isAppShell(context, shell))
90
+ return false;
91
+ }
92
+ return true;
93
+ };
@@ -0,0 +1,13 @@
1
+ /** Traverse only the visible bar composition and its local React owners. */
2
+ import type { TSESTree } from "@typescript-eslint/types";
3
+ import type { FunctionNode, Node, ReaderContext, Resolved } from "./types.ts";
4
+ /** Which side of a conditional the traversal follows: one branch, or `both`. */
5
+ export type Choice = "consequent" | "alternate" | "both";
6
+ /** Called for every visible opening element; returning `false` skips its slots and children. */
7
+ export type Visit = (opening: TSESTree.JSXOpeningElement, name: string | null, ancestors: readonly TSESTree.JSXOpeningElement[]) => boolean | void;
8
+ /** Decides, per conditional node, which branch to follow. */
9
+ export type Branch = (node: Node) => Choice;
10
+ /** Visible JSX composition only: menu, popover and drawer bodies are outside the navbar band. */
11
+ export declare const visitVisible: (context: ReaderContext, expression: Resolved | null | undefined, visit: Visit, seen?: Set<Resolved>, branch?: Branch, ancestors?: readonly TSESTree.JSXOpeningElement[]) => void;
12
+ /** Visit a rendered owner's returns with the same branch, overlay and local-binding semantics as inline JSX. */
13
+ export declare const visitFunction: (context: ReaderContext, fn: FunctionNode, visit: Visit, seen: Set<Resolved>, branch: Branch, ancestors: readonly TSESTree.JSXOpeningElement[]) => void;
@@ -0,0 +1,117 @@
1
+ import { functionOf, variableOf } from "@starci/eslint-canon-fe/lib/bindings.mjs";
2
+ import { grammarName, propOf, realNode, resolveLocal } from "./app-shell.js";
3
+ const BAR_SLOTS = {
4
+ WorkspaceShell: ["header", "compactHeader"],
5
+ TopBar: ["brand", "navigation", "actions"],
6
+ NavigationFeatureNav: ["identity", "navigation", "actions", "compactNavigationTrigger"],
7
+ DropdownMenu: ["trigger"],
8
+ Popover: ["trigger"],
9
+ Drawer: ["trigger"],
10
+ };
11
+ /** Visible JSX composition only: menu, popover and drawer bodies are outside the navbar band. */
12
+ export const visitVisible = (context, expression, visit, seen = new Set(), branch = () => "both", ancestors = []) => {
13
+ const node = resolveLocal(context, expression);
14
+ if (!node || seen.has(node))
15
+ return;
16
+ const visited = new Set(seen).add(node);
17
+ const next = (child) => visitVisible(context, child, visit, visited, branch, ancestors);
18
+ switch (node.type) {
19
+ case "JSXExpressionContainer":
20
+ next(node.expression);
21
+ break;
22
+ case "JSXFragment":
23
+ for (const child of node.children)
24
+ next(child);
25
+ break;
26
+ case "ConditionalExpression":
27
+ visitConditional(node, next, branch);
28
+ break;
29
+ case "LogicalExpression":
30
+ visitLogical(context, node, next, branch);
31
+ break;
32
+ case "ArrayExpression":
33
+ for (const item of node.elements)
34
+ next(item);
35
+ break;
36
+ case "JSXElement":
37
+ visitElement(context, node, visit, visited, branch, ancestors);
38
+ break;
39
+ default:
40
+ }
41
+ };
42
+ const visitConditional = (node, next, branch) => {
43
+ const choice = branch(node);
44
+ if (choice !== "alternate")
45
+ next(node.consequent);
46
+ if (choice !== "consequent")
47
+ next(node.alternate);
48
+ };
49
+ const visitLogical = (context, node, next, branch) => {
50
+ const left = resolveLocal(context, node.left);
51
+ const known = left?.type === "Literal";
52
+ const leftValue = left?.type === "Literal" ? left.value : undefined;
53
+ let right = node.operator === "&&" ? Boolean(leftValue) : !leftValue;
54
+ if (node.operator === "??")
55
+ right = leftValue === null;
56
+ if (!known) {
57
+ const choice = branch(node);
58
+ if (choice === "both") {
59
+ next(node.left);
60
+ next(node.right);
61
+ return;
62
+ }
63
+ right = choice === "consequent";
64
+ }
65
+ next(right ? node.right : node.left);
66
+ };
67
+ const visitElement = (context, node, visit, seen, branch, ancestors) => {
68
+ const opening = node.openingElement;
69
+ const name = grammarName(context, opening);
70
+ if (visit(opening, name, ancestors) === false)
71
+ return;
72
+ const inside = [...ancestors, opening];
73
+ const next = (child) => visitVisible(context, child, visit, seen, branch, inside);
74
+ if (name === null && opening.name.type === "JSXIdentifier") {
75
+ const fn = functionOf(context, realNode(resolveLocal(context, opening.name))) ?? variableOf(context, opening.name)?.defs.find((definition) => definition.type === "FunctionName")?.node;
76
+ if (fn)
77
+ visitFunction(context, fn, visit, seen, branch, inside);
78
+ }
79
+ const slots = name === null ? undefined : BAR_SLOTS[name];
80
+ if (slots)
81
+ for (const slot of slots)
82
+ next(propOf(context, opening, slot).value);
83
+ if (name !== null && ["DropdownMenu", "Popover", "Drawer"].includes(name))
84
+ return;
85
+ for (const child of node.children)
86
+ next(child);
87
+ };
88
+ /** Visit a rendered owner's returns with the same branch, overlay and local-binding semantics as inline JSX. */
89
+ export const visitFunction = (context, fn, visit, seen, branch, ancestors) => {
90
+ if (fn.body.type !== "BlockStatement")
91
+ return visitVisible(context, fn.body, visit, seen, branch, ancestors);
92
+ visitStatements(context, fn.body.body, visit, seen, branch, ancestors);
93
+ };
94
+ const visitStatements = (context, statements, visit, seen, branch, ancestors) => {
95
+ for (const statement of statements) {
96
+ if (statement.type === "ReturnStatement") {
97
+ visitVisible(context, statement.argument, visit, seen, branch, ancestors);
98
+ return true;
99
+ }
100
+ if (statement.type === "BlockStatement" && visitStatements(context, statement.body, visit, seen, branch, ancestors))
101
+ return true;
102
+ if (statement.type === "IfStatement") {
103
+ if (visitIf(context, statement, visit, seen, branch, ancestors))
104
+ return true;
105
+ }
106
+ }
107
+ return false;
108
+ };
109
+ const visitIf = (context, statement, visit, seen, branch, ancestors) => {
110
+ const condition = resolveLocal(context, statement.test);
111
+ let choice = branch(statement);
112
+ if (condition?.type === "Literal")
113
+ choice = condition.value ? "consequent" : "alternate";
114
+ const consequent = choice !== "alternate" && visitStatements(context, [statement.consequent], visit, seen, branch, ancestors);
115
+ const alternate = choice !== "consequent" && statement.alternate && visitStatements(context, [statement.alternate], visit, seen, branch, ancestors);
116
+ return choice === "consequent" && consequent || choice === "alternate" && alternate || consequent && alternate;
117
+ };
@@ -0,0 +1,27 @@
1
+ /** Static readers for the visible Grammar app-bar composition, including local JSX owners. */
2
+ import type { TSESTree } from "@typescript-eslint/types";
3
+ import type { Node, ReaderContext, Resolved } from "./types.ts";
4
+ /** The effective value of a prop: `known` is false when an unresolvable spread could still supply it. */
5
+ export interface PropValue {
6
+ readonly known: boolean;
7
+ readonly value: Node | null;
8
+ readonly present?: true;
9
+ }
10
+ /** Bar policy also counts labelled/value/selection/action field owners, so an extra control cannot vanish from the cluster. */
11
+ export declare const isBarControl: (context: ReaderContext, opening: TSESTree.JSXOpeningElement, name: string | null) => boolean;
12
+ /** A literal or constant's initializer, without type-only wrappers. */
13
+ export declare const resolveLocal: (context: ReaderContext, expression: Resolved | null | undefined, seen?: Set<Resolved>) => Resolved | null | undefined;
14
+ /** A resolved value as a parsed node; a synthesized member stands for no function or value of the tree. */
15
+ export declare const realNode: (value: Resolved | null | undefined) => Node | null | undefined;
16
+ /** The imported Grammar component's name; aliases and namespace imports keep their identity. */
17
+ export declare const grammarName: (context: ReaderContext, opening: TSESTree.JSXOpeningElement) => string | null;
18
+ /** The text a property key spells: an identifier's name or a string literal's value; null for any other key. */
19
+ export declare const propertyKeyText: (key: Node) => string | null;
20
+ /** The effective prop, respecting JSX spread order; unknown later spreads cannot prove a value. */
21
+ export declare const propOf: (context: ReaderContext, opening: TSESTree.JSXOpeningElement, name: string) => PropValue;
22
+ /** A statically proven string prop, or null for missing/unknown values. */
23
+ export declare const textProp: (context: ReaderContext, opening: TSESTree.JSXOpeningElement, name: string) => string | null;
24
+ /** The source spelling is semantic evidence for an icon or action whose translated label is dynamic. */
25
+ export declare const semanticText: (context: ReaderContext, opening: TSESTree.JSXOpeningElement) => string;
26
+ /** The right-cluster role and the Grammar kind that must draw it. */
27
+ export declare const clusterRole: (context: ReaderContext, opening: TSESTree.JSXOpeningElement, name: string | null) => string | null;