@shapeshift-labs/frontier-lang-compiler 0.2.197 → 0.2.198

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/README.md CHANGED
@@ -611,7 +611,8 @@ Recent residual closures represented in the matrix shards:
611
611
  - Module/export/import graph now records no-substitution template dynamic imports and parser-backed CommonJS helper `require` templates as static module edges while keeping expression templates proof-required.
612
612
  - Module/export/import graph now records no-substitution/static template host dependencies for `new URL`, workers, resolver calls, and `importScripts` while leaving expression templates unsupported.
613
613
  - Module/export/import graph now emits dynamic host dependency targets as `<host-dependency>` edges with expression hashes and proof-required unresolved evidence instead of omitting them.
614
- - CSS Modules graph records now infer local class export evidence, source-local composition graph hashes, export-only ICSS graph hashes, and project-source graph hashes for cross-file `composes` / ICSS imports when every referenced `.module.css` source is present and source-inferred; package/bundler-only composition, unresolved ICSS imports, generated class-name maps, bundler transform identity, source-map proof, dynamic member access, writes, and helper-call equivalence remain fail-closed without host evidence.
614
+ - CSS Modules graph records now infer local class export evidence, source-local composition graph hashes, export-only ICSS graph hashes, project-source graph hashes for cross-file `composes` / ICSS imports when every referenced `.module.css` source is present and source-inferred, and source-bounded class helper token graph hashes for single top-level `className` helper calls whose CSS Module reads are statically enumerable; package/bundler-only composition, unresolved ICSS imports, generated class-name maps, bundler transform identity, source-map proof, dynamic member access, writes, nested helper calls, and arbitrary helper runtime equivalence remain fail-closed without host evidence.
615
+ - CSS Modules use-site graph evidence now suppresses `css-module-helper-call-unproved` only for source-bounded helper calls with deterministic token enumeration, while preserving transform proof blockers and keeping nested calls/dynamic keys on review routes.
615
616
  - Type/public API graph now includes bounded TypeChecker assignability oracle proof for simple public type aliases while keeping semantic/runtime equivalence claims false.
616
617
  - JSX/TSX graph now records same-file and project-local named/default/barrel-import static component prop passthrough as `jsx-render-component-prop-flow-static-passthrough-evidence` and blocks dynamic callsite values with `jsx-render-component-prop-flow-dynamic-value-unsupported`.
617
618
  - JSX/TSX graph now records static `memo` / `forwardRef` / `observer` / `React.lazy` component wrapper chains as component-wrapper render-risk evidence while keeping render, lazy-load, and runtime equivalence explicitly unproved.
@@ -51,6 +51,13 @@ export interface NativeProjectSymbolGraphCssModuleUseSiteRecord {
51
51
  readonly jsxPropRecordId?: string;
52
52
  readonly scopeReferenceRecordId?: string;
53
53
  readonly conditionalRuntimePresence?: boolean;
54
+ readonly helperCallProofLevel?: 'css-module-class-helper-source-bounded-token-graph' | string;
55
+ readonly helperCallGraphHash?: string;
56
+ readonly helperCalleeName?: string;
57
+ readonly helperCalleeRoot?: string;
58
+ readonly helperCalleeSource?: 'known-package-import' | 'local-name-convention' | string;
59
+ readonly helperModuleSpecifier?: string;
60
+ readonly helperImportEdgeId?: string;
54
61
  readonly signatureHash?: string;
55
62
  }
56
63
 
@@ -91,6 +98,9 @@ export interface NativeProjectSymbolGraphCssModuleUseSiteGraphRecord {
91
98
  readonly cssModuleCompositionGraphSource?: 'supplied' | 'source-local' | 'project-source' | string;
92
99
  readonly icssGraphHash?: string;
93
100
  readonly icssGraphSource?: 'supplied' | 'source-export-only' | 'project-source' | string;
101
+ readonly cssModuleClassNameHelperGraphHash?: string;
102
+ readonly cssModuleClassNameHelperProofLevels?: readonly string[];
103
+ readonly cssModuleClassNameHelperSources?: readonly string[];
94
104
  readonly bundlerTransformHash?: string;
95
105
  readonly sourceMapProofHash?: string;
96
106
  readonly status: 'ready' | 'blocked' | string;
@@ -0,0 +1,219 @@
1
+ import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
+ import { compactRecord, localKey } from './projectSymbolGraphCssModuleUtils.js';
3
+ import { cssModuleMemberAccess, identifierOccurrences } from './projectSymbolGraphCssModuleMemberAccess.js';
4
+
5
+ const KnownClassHelperModules = new Set(['clsx', 'classnames']);
6
+ const LocalClassHelperNames = new Set(['cx', 'cn', 'clsx', 'classnames', 'classNames']);
7
+
8
+ function cssModuleClassHelperCalleeEvidence(importEdges = []) {
9
+ const result = new Map();
10
+ for (const edge of importEdges ?? []) {
11
+ if (!edge?.sourcePath || !edge.localName || !KnownClassHelperModules.has(edge.moduleSpecifier)) continue;
12
+ if (String(edge.importKind ?? '').startsWith('type')) continue;
13
+ result.set(localKey(edge.sourcePath, edge.localName), compactRecord({
14
+ helperCalleeName: edge.localName,
15
+ helperCalleeRoot: edge.localName,
16
+ helperCalleeSource: 'known-package-import',
17
+ helperModuleSpecifier: edge.moduleSpecifier,
18
+ helperImportKind: edge.importKind,
19
+ helperImportedName: edge.importedName,
20
+ helperImportEdgeId: edge.id
21
+ }));
22
+ }
23
+ return result;
24
+ }
25
+
26
+ function cssModuleClassHelperCallProof(expressionText, binding, prop, helperCalleeEvidence) {
27
+ const call = readTopLevelClassHelperCall(expressionText);
28
+ if (!call) return undefined;
29
+ const calleeEvidence = helperCalleeEvidence.get(localKey(prop.sourcePath, call.calleeRoot))
30
+ ?? localClassHelperCalleeEvidence(call);
31
+ if (!calleeEvidence) return undefined;
32
+ if (classHelperCallHasUnsafeSyntax(expressionText, call.open)) return undefined;
33
+ const accessRecords = cssModuleHelperAccessRecords(expressionText, binding);
34
+ if (!accessRecords.length) return undefined;
35
+ const helperCallGraphHash = hashSemanticValue({
36
+ kind: 'frontier.lang.cssModuleClassNameHelperCallGraph.v1',
37
+ proofLevel: 'css-module-class-helper-source-bounded-token-graph',
38
+ sourcePath: prop.sourcePath,
39
+ sourceHash: prop.sourceHash,
40
+ cssModuleImportBindingId: binding.id,
41
+ cssModuleSourcePath: binding.cssModuleSourcePath,
42
+ cssModuleHash: binding.cssModuleHash,
43
+ jsxPropRecordId: prop.id,
44
+ helperCalleeName: call.calleeText,
45
+ helperCalleeRoot: call.calleeRoot,
46
+ helperCalleeSource: calleeEvidence.helperCalleeSource,
47
+ helperModuleSpecifier: calleeEvidence.helperModuleSpecifier,
48
+ expressionText,
49
+ accessRecords
50
+ });
51
+ return compactRecord({
52
+ proofLevel: 'css-module-class-helper-source-bounded-token-graph',
53
+ helperCallGraphHash,
54
+ helperCalleeName: call.calleeText,
55
+ helperCalleeRoot: call.calleeRoot,
56
+ helperCalleeSource: calleeEvidence.helperCalleeSource,
57
+ helperModuleSpecifier: calleeEvidence.helperModuleSpecifier,
58
+ helperImportEdgeId: calleeEvidence.helperImportEdgeId
59
+ });
60
+ }
61
+
62
+ function cssModuleHelperArgumentIsConditional(expressionText, occurrenceStart) {
63
+ const prefix = expressionText.slice(0, occurrenceStart);
64
+ const suffix = expressionText.slice(occurrenceStart);
65
+ const lastComma = prefix.lastIndexOf(',');
66
+ const argumentPrefix = prefix.slice(lastComma + 1);
67
+ const nextComma = suffix.indexOf(',');
68
+ const argumentText = `${argumentPrefix}${nextComma === -1 ? suffix : suffix.slice(0, nextComma)}`;
69
+ return /(?:&&|\|\||\?)/.test(argumentText);
70
+ }
71
+
72
+ function cssModuleMemberWriteOperation(sourceText, start, end) {
73
+ if (hasPrefixUpdateOperator(sourceText, start)) return 'update';
74
+ let index = end;
75
+ while (index < sourceText.length && /\s/.test(sourceText[index])) index += 1;
76
+ if (sourceText.slice(index, index + 2) === '++' || sourceText.slice(index, index + 2) === '--') return 'update';
77
+ return assignmentOperatorAt(sourceText, index) ? 'assignment' : undefined;
78
+ }
79
+
80
+ function localClassHelperCalleeEvidence(call) {
81
+ return LocalClassHelperNames.has(call.calleeRoot)
82
+ ? { helperCalleeName: call.calleeText, helperCalleeRoot: call.calleeRoot, helperCalleeSource: 'local-name-convention' }
83
+ : undefined;
84
+ }
85
+
86
+ function cssModuleHelperAccessRecords(expressionText, binding) {
87
+ const records = [];
88
+ for (const occurrence of identifierOccurrences(expressionText, binding.localName)) {
89
+ const access = cssModuleMemberAccess(expressionText, occurrence.end);
90
+ if (!access || access.status === 'blocked') return [];
91
+ if (cssModuleMemberWriteOperation(expressionText, occurrence.start, access.end)) return [];
92
+ records.push({
93
+ receiverLocalName: binding.localName,
94
+ exportName: access.memberName,
95
+ accessKind: access.accessKind,
96
+ expressionText: expressionText.slice(occurrence.start, access.end),
97
+ start: occurrence.start,
98
+ end: access.end,
99
+ conditionalRuntimePresence: cssModuleHelperArgumentIsConditional(expressionText, occurrence.start) || undefined
100
+ });
101
+ }
102
+ return records;
103
+ }
104
+
105
+ function readTopLevelClassHelperCall(expressionText) {
106
+ const value = String(expressionText ?? '').trim();
107
+ const callee = /^(?:[A-Za-z_$][\w$]*|this)(?:\s*(?:\.|\?\.)\s*[A-Za-z_$][\w$]*)*/.exec(value)?.[0];
108
+ if (!callee) return undefined;
109
+ let index = callee.length;
110
+ while (index < value.length && /\s/.test(value[index])) index += 1;
111
+ if (value[index] !== '(') return undefined;
112
+ const close = findMatchingDelimiter(value, index, '(', ')');
113
+ if (close === -1) return undefined;
114
+ let tail = close + 1;
115
+ while (tail < value.length && /\s/.test(value[tail])) tail += 1;
116
+ if (tail !== value.length) return undefined;
117
+ const calleeText = callee.replace(/\s+/g, '');
118
+ return { calleeText, calleeRoot: calleeText.split(/[?.]+/)[0], open: index };
119
+ }
120
+
121
+ function classHelperCallHasUnsafeSyntax(expressionText, topLevelOpen) {
122
+ const value = String(expressionText ?? '');
123
+ if (value.includes('...') || value.includes('=>') || /\b(?:function|class|new|await|yield)\b/.test(value)) return true;
124
+ if (/`[\s\S]*\$\{[\s\S]*`/.test(value)) return true;
125
+ if (/(?:\+\+|--|\*\*=|>>>=|<<=|>>=|&&=|\|\|=|\?\?=|\+=|-=|\*=|\/=|%=|&=|\|=|\^=)/.test(value)) return true;
126
+ if (/(^|[^=!<>])=(?!=|>)/.test(value)) return true;
127
+ for (const callOpen of callOpenIndexes(value)) if (callOpen !== topLevelOpen) return true;
128
+ return !delimitersAreBalanced(value);
129
+ }
130
+
131
+ function callOpenIndexes(sourceText) {
132
+ const indexes = [];
133
+ for (let index = 0; index < sourceText.length; index += 1) {
134
+ const char = sourceText[index];
135
+ if (char === '\'' || char === '"' || char === '`') {
136
+ const end = readQuotedOrStaticTemplateEnd(sourceText, index, char);
137
+ if (end === -1) return [...indexes, -1];
138
+ index = end;
139
+ continue;
140
+ }
141
+ if (char !== '(') continue;
142
+ let before = index - 1;
143
+ while (before >= 0 && /\s/.test(sourceText[before])) before -= 1;
144
+ if (before >= 0 && /[A-Za-z0-9_$\].]/.test(sourceText[before])) indexes.push(index);
145
+ }
146
+ return indexes;
147
+ }
148
+
149
+ function delimitersAreBalanced(sourceText) {
150
+ const stack = [];
151
+ const pairs = { '(': ')', '[': ']', '{': '}' };
152
+ for (let index = 0; index < sourceText.length; index += 1) {
153
+ const char = sourceText[index];
154
+ if (char === '\'' || char === '"' || char === '`') {
155
+ const end = readQuotedOrStaticTemplateEnd(sourceText, index, char);
156
+ if (end === -1) return false;
157
+ index = end;
158
+ continue;
159
+ }
160
+ if (pairs[char]) stack.push(pairs[char]);
161
+ else if ((char === ')' || char === ']' || char === '}') && stack.pop() !== char) return false;
162
+ }
163
+ return stack.length === 0;
164
+ }
165
+
166
+ function findMatchingDelimiter(sourceText, open, openChar, closeChar) {
167
+ let depth = 0;
168
+ for (let index = open; index < sourceText.length; index += 1) {
169
+ const char = sourceText[index];
170
+ if (char === '\'' || char === '"' || char === '`') {
171
+ const end = readQuotedOrStaticTemplateEnd(sourceText, index, char);
172
+ if (end === -1) return -1;
173
+ index = end;
174
+ continue;
175
+ }
176
+ if (char === openChar) depth += 1;
177
+ else if (char === closeChar && --depth === 0) return index;
178
+ }
179
+ return -1;
180
+ }
181
+
182
+ function readQuotedOrStaticTemplateEnd(sourceText, start, quote) {
183
+ for (let index = start + 1; index < sourceText.length; index += 1) {
184
+ const char = sourceText[index];
185
+ if (char === '\\') {
186
+ index += 1;
187
+ continue;
188
+ }
189
+ if (quote === '`' && char === '$' && sourceText[index + 1] === '{') return -1;
190
+ if (char === quote) return index;
191
+ }
192
+ return -1;
193
+ }
194
+
195
+ function assignmentOperatorAt(code, index) {
196
+ for (const operator of ['>>>=', '**=', '<<=', '>>=', '&&=', '||=', '??=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '=']) {
197
+ if (!code.startsWith(operator, index)) continue;
198
+ if (operator === '=' && (code[index + 1] === '=' || code[index + 1] === '>')) return undefined;
199
+ return operator;
200
+ }
201
+ return undefined;
202
+ }
203
+
204
+ function hasPrefixUpdateOperator(code, tokenStart) {
205
+ let index = tokenStart - 1;
206
+ while (index >= 0 && /\s/.test(code[index])) index -= 1;
207
+ const operator = code.slice(index - 1, index + 1);
208
+ if (operator !== '++' && operator !== '--') return false;
209
+ let before = index - 2;
210
+ while (before >= 0 && /\s/.test(code[before])) before -= 1;
211
+ return before < 0 || !/[A-Za-z0-9_$)\]]/.test(code[before]);
212
+ }
213
+
214
+ export {
215
+ cssModuleClassHelperCalleeEvidence,
216
+ cssModuleClassHelperCallProof,
217
+ cssModuleHelperArgumentIsConditional,
218
+ cssModuleMemberWriteOperation
219
+ };
@@ -0,0 +1,89 @@
1
+ import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
+ import { semanticSpanForHash } from './projectSymbolGraphCssModuleUtils.js';
3
+
4
+ function cssModuleUseSiteGraphHashes(cssModuleSourcePath, bindings, graphUseSites, graphBlockers) {
5
+ const helperGraph = cssModuleClassNameHelperGraphSummary(graphUseSites);
6
+ return {
7
+ jsTsUseSiteGraphHash: hashSemanticValue({
8
+ kind: 'frontier.lang.cssModuleJsTsUseSiteGraph.v1',
9
+ cssModuleSourcePath,
10
+ bindings: bindings.map(cssModuleUseSiteBindingSignature).sort(stableStringCompare),
11
+ useSites: graphUseSites.map(cssModuleUseSiteSignature).sort(stableStringCompare),
12
+ blockers: graphBlockers.map(cssModuleUseSiteBlockerSignature).sort(stableStringCompare)
13
+ }),
14
+ graphHash: hashSemanticValue({
15
+ kind: 'frontier.lang.cssModuleUseSiteGraph.v1',
16
+ cssModuleSourcePath,
17
+ bindings: bindings.map((binding) => binding.signatureHash).sort(),
18
+ useSites: graphUseSites.map((site) => site.signatureHash).sort(),
19
+ blockers: graphBlockers.map((blocker) => blocker.signatureHash).sort()
20
+ }),
21
+ ...helperGraph
22
+ };
23
+ }
24
+
25
+ function cssModuleUseSiteBindingSignature(binding) {
26
+ return JSON.stringify({
27
+ sourcePath: binding.sourcePath,
28
+ sourceHash: binding.sourceHash,
29
+ moduleSpecifier: binding.moduleSpecifier,
30
+ resolvedModulePath: binding.resolvedModulePath,
31
+ importKind: binding.importKind,
32
+ importedName: binding.importedName,
33
+ localName: binding.localName
34
+ });
35
+ }
36
+
37
+ function cssModuleUseSiteSignature(site) {
38
+ return JSON.stringify({
39
+ jsSourcePath: site.jsSourcePath,
40
+ jsSourceHash: site.jsSourceHash,
41
+ exportName: site.exportName,
42
+ useSiteKind: site.useSiteKind,
43
+ accessKind: site.accessKind,
44
+ receiverLocalName: site.receiverLocalName,
45
+ localReferenceName: site.localReferenceName,
46
+ expressionText: site.expressionText,
47
+ sourceSpan: semanticSpanForHash(site.sourceSpan),
48
+ conditionalRuntimePresence: site.conditionalRuntimePresence,
49
+ helperCallProofLevel: site.helperCallProofLevel,
50
+ helperCallGraphHash: site.helperCallGraphHash,
51
+ helperCalleeName: site.helperCalleeName,
52
+ helperCalleeSource: site.helperCalleeSource,
53
+ helperModuleSpecifier: site.helperModuleSpecifier
54
+ });
55
+ }
56
+
57
+ function cssModuleUseSiteBlockerSignature(blocker) {
58
+ return JSON.stringify({
59
+ sourcePath: blocker.sourcePath,
60
+ sourceHash: blocker.sourceHash,
61
+ moduleSpecifier: blocker.moduleSpecifier,
62
+ localName: blocker.localName,
63
+ expressionText: blocker.expressionText,
64
+ reasonCode: blocker.reasonCode,
65
+ writeOperation: blocker.writeOperation,
66
+ sourceSpan: semanticSpanForHash(blocker.sourceSpan)
67
+ });
68
+ }
69
+
70
+ function cssModuleClassNameHelperGraphSummary(useSites) {
71
+ const helperHashes = uniqueSortedStrings(useSites.map((site) => site.helperCallGraphHash));
72
+ if (!helperHashes.length) return {};
73
+ return {
74
+ cssModuleClassNameHelperGraphHash: hashSemanticValue({
75
+ kind: 'frontier.lang.cssModuleClassNameHelperGraph.v1',
76
+ helperCallGraphHashes: helperHashes
77
+ }),
78
+ cssModuleClassNameHelperProofLevels: uniqueSortedStrings(useSites.map((site) => site.helperCallProofLevel)),
79
+ cssModuleClassNameHelperSources: uniqueSortedStrings(useSites.map((site) => site.helperCalleeSource))
80
+ };
81
+ }
82
+
83
+ function stableStringCompare(left, right) { return left.localeCompare(right); }
84
+
85
+ function uniqueSortedStrings(values) {
86
+ return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))].sort();
87
+ }
88
+
89
+ export { cssModuleUseSiteGraphHashes };
@@ -7,6 +7,7 @@ import {
7
7
  semanticSpanForHash,
8
8
  sourceSpanForRange
9
9
  } from './projectSymbolGraphCssModuleUtils.js';
10
+ import { cssModuleUseSiteGraphHashes } from './projectSymbolGraphCssModuleGraphHashes.js';
10
11
 
11
12
  function cssModuleImportBindingRecord(edge, index, documentsById, cssSourcesByPath) {
12
13
  const cssSourcePath = edge.resolvedModulePath ?? cssModuleSpecifierPath(edge.moduleSpecifier);
@@ -71,7 +72,12 @@ function cssModuleUseSiteRecord(binding, input) {
71
72
  accessKind: input.accessKind,
72
73
  exportName: input.exportName,
73
74
  expressionText: input.expressionText,
74
- sourceSpan: semanticSpanForHash(input.sourceSpan)
75
+ sourceSpan: semanticSpanForHash(input.sourceSpan),
76
+ helperCallProofLevel: input.helperCallProofLevel,
77
+ helperCallGraphHash: input.helperCallGraphHash,
78
+ helperCalleeName: input.helperCalleeName,
79
+ helperCalleeSource: input.helperCalleeSource,
80
+ helperModuleSpecifier: input.helperModuleSpecifier
75
81
  });
76
82
  return compactRecord({
77
83
  id: `css_module_use_${idFragment(binding.id)}_${idFragment(signatureHash)}`,
@@ -93,6 +99,13 @@ function cssModuleUseSiteRecord(binding, input) {
93
99
  jsxPropRecordId: input.jsxPropRecordId,
94
100
  scopeReferenceRecordId: input.scopeReferenceRecordId,
95
101
  conditionalRuntimePresence: input.conditionalRuntimePresence,
102
+ helperCallProofLevel: input.helperCallProofLevel,
103
+ helperCallGraphHash: input.helperCallGraphHash,
104
+ helperCalleeName: input.helperCalleeName,
105
+ helperCalleeRoot: input.helperCalleeRoot,
106
+ helperCalleeSource: input.helperCalleeSource,
107
+ helperModuleSpecifier: input.helperModuleSpecifier,
108
+ helperImportEdgeId: input.helperImportEdgeId,
96
109
  signatureHash
97
110
  });
98
111
  }
@@ -187,20 +200,7 @@ function cssModuleUseSiteGraphRecords(importBindings, useSites, blockers) {
187
200
  }
188
201
 
189
202
  function cssModuleUseSiteGraphRecord(cssModuleSourcePath, bindings, graphUseSites, graphBlockers) {
190
- const jsTsUseSiteGraphHash = hashSemanticValue({
191
- kind: 'frontier.lang.cssModuleJsTsUseSiteGraph.v1',
192
- cssModuleSourcePath,
193
- bindings: bindings.map(cssModuleUseSiteBindingSignature).sort(stableStringCompare),
194
- useSites: graphUseSites.map(cssModuleUseSiteSignature).sort(stableStringCompare),
195
- blockers: graphBlockers.map(cssModuleUseSiteBlockerSignature).sort(stableStringCompare)
196
- });
197
- const graphHash = hashSemanticValue({
198
- kind: 'frontier.lang.cssModuleUseSiteGraph.v1',
199
- cssModuleSourcePath,
200
- bindings: bindings.map((binding) => binding.signatureHash).sort(),
201
- useSites: graphUseSites.map((site) => site.signatureHash).sort(),
202
- blockers: graphBlockers.map((blocker) => blocker.signatureHash).sort()
203
- });
203
+ const graphHashes = cssModuleUseSiteGraphHashes(cssModuleSourcePath, bindings, graphUseSites, graphBlockers);
204
204
  return compactRecord({
205
205
  kind: 'frontier.lang.cssModuleUseSiteGraph',
206
206
  version: 1,
@@ -219,58 +219,19 @@ function cssModuleUseSiteGraphRecord(cssModuleSourcePath, bindings, graphUseSite
219
219
  cssModuleCompositionGraphSource: bindings.find((binding) => binding.cssModuleCompositionGraphSource)?.cssModuleCompositionGraphSource,
220
220
  icssGraphHash: bindings.find((binding) => binding.icssGraphHash)?.icssGraphHash,
221
221
  icssGraphSource: bindings.find((binding) => binding.icssGraphSource)?.icssGraphSource,
222
+ cssModuleClassNameHelperGraphHash: graphHashes.cssModuleClassNameHelperGraphHash,
223
+ cssModuleClassNameHelperProofLevels: graphHashes.cssModuleClassNameHelperProofLevels,
224
+ cssModuleClassNameHelperSources: graphHashes.cssModuleClassNameHelperSources,
222
225
  bundlerTransformHash: bindings.find((binding) => binding.bundlerTransformHash)?.bundlerTransformHash,
223
226
  sourceMapProofHash: bindings.find((binding) => binding.sourceMapProofHash)?.sourceMapProofHash,
224
- jsTsUseSiteGraphHash,
227
+ jsTsUseSiteGraphHash: graphHashes.jsTsUseSiteGraphHash,
225
228
  status: graphBlockers.length ? 'blocked' : 'ready',
226
- graphHash,
229
+ graphHash: graphHashes.graphHash,
227
230
  autoMergeClaim: false,
228
231
  semanticEquivalenceClaim: false
229
232
  });
230
233
  }
231
234
 
232
- function cssModuleUseSiteBindingSignature(binding) {
233
- return JSON.stringify({
234
- sourcePath: binding.sourcePath,
235
- sourceHash: binding.sourceHash,
236
- moduleSpecifier: binding.moduleSpecifier,
237
- resolvedModulePath: binding.resolvedModulePath,
238
- importKind: binding.importKind,
239
- importedName: binding.importedName,
240
- localName: binding.localName
241
- });
242
- }
243
-
244
- function cssModuleUseSiteSignature(site) {
245
- return JSON.stringify({
246
- jsSourcePath: site.jsSourcePath,
247
- jsSourceHash: site.jsSourceHash,
248
- exportName: site.exportName,
249
- useSiteKind: site.useSiteKind,
250
- accessKind: site.accessKind,
251
- receiverLocalName: site.receiverLocalName,
252
- localReferenceName: site.localReferenceName,
253
- expressionText: site.expressionText,
254
- sourceSpan: semanticSpanForHash(site.sourceSpan),
255
- conditionalRuntimePresence: site.conditionalRuntimePresence
256
- });
257
- }
258
-
259
- function cssModuleUseSiteBlockerSignature(blocker) {
260
- return JSON.stringify({
261
- sourcePath: blocker.sourcePath,
262
- sourceHash: blocker.sourceHash,
263
- moduleSpecifier: blocker.moduleSpecifier,
264
- localName: blocker.localName,
265
- expressionText: blocker.expressionText,
266
- reasonCode: blocker.reasonCode,
267
- writeOperation: blocker.writeOperation,
268
- sourceSpan: semanticSpanForHash(blocker.sourceSpan)
269
- });
270
- }
271
-
272
- function stableStringCompare(left, right) { return left.localeCompare(right); }
273
-
274
235
  function cssModuleExportHash(binding, exportName) {
275
236
  if (!exportName || !Array.isArray(binding.cssModuleExportNames) || !binding.cssModuleExportNames.includes(exportName)) return undefined;
276
237
  return hashSemanticValue({ kind: 'frontier.lang.cssModuleExport.v1', cssModuleHash: binding.cssModuleHash, exportName });
@@ -16,6 +16,12 @@ import {
16
16
  cssModuleStaticExpressionAccess,
17
17
  identifierOccurrences
18
18
  } from './projectSymbolGraphCssModuleMemberAccess.js';
19
+ import {
20
+ cssModuleClassHelperCalleeEvidence,
21
+ cssModuleClassHelperCallProof,
22
+ cssModuleHelperArgumentIsConditional,
23
+ cssModuleMemberWriteOperation
24
+ } from './projectSymbolGraphCssModuleClassHelpers.js';
19
25
 
20
26
  function cssModuleLexicalUseSites(importBindings, sourceTextsByPath) {
21
27
  const useSites = [];
@@ -43,7 +49,7 @@ function cssModuleMemberUseSites(binding, sourceText, blockers) {
43
49
  blockers.push(cssModuleAccessBlocker(binding, sourceText, occurrence.start, access.end ?? occurrence.end, access.reasonCode, access.expressionText));
44
50
  continue;
45
51
  }
46
- const writeOperation = memberWriteOperation(sourceText, occurrence.start, access.end);
52
+ const writeOperation = cssModuleMemberWriteOperation(sourceText, occurrence.start, access.end);
47
53
  if (writeOperation) {
48
54
  blockers.push(cssModuleAccessBlocker(binding, sourceText, occurrence.start, access.end, 'css-module-member-write-unsupported', sourceText.slice(occurrence.start, access.end), { writeOperation }));
49
55
  continue;
@@ -93,9 +99,10 @@ function cssModuleDestructuringUseSites(binding, sourceText, blockers) {
93
99
  return useSites;
94
100
  }
95
101
 
96
- function cssModuleJsxUseSites(bindingsByLocal, jsxPropRecords) {
102
+ function cssModuleJsxUseSites(bindingsByLocal, jsxPropRecords, importEdges = []) {
97
103
  const useSites = [];
98
104
  const blockers = [];
105
+ const helperCalleeEvidence = cssModuleClassHelperCalleeEvidence(importEdges);
99
106
  for (const prop of jsxPropRecords ?? []) {
100
107
  const root = prop.propValueReferenceRoot;
101
108
  const sourceBindings = root ? bindingsByLocal.get(localKey(prop.sourcePath, root)) ?? [] : [];
@@ -103,8 +110,8 @@ function cssModuleJsxUseSites(bindingsByLocal, jsxPropRecords) {
103
110
  const classNameBindings = bindingsForSourcePath(bindingsByLocal, prop.sourcePath);
104
111
  if (prop.propName !== 'className' || !classNameBindings.length) continue;
105
112
  useSites.push(...cssModuleJsxStaticComputedUseSites(classNameBindings, prop));
106
- useSites.push(...cssModuleJsxHelperUseSites(classNameBindings, prop));
107
- blockers.push(...cssModuleJsxBlockers(classNameBindings, prop));
113
+ useSites.push(...cssModuleJsxHelperUseSites(classNameBindings, prop, helperCalleeEvidence));
114
+ blockers.push(...cssModuleJsxBlockers(classNameBindings, prop, helperCalleeEvidence));
108
115
  }
109
116
  return { useSites, blockers };
110
117
  }
@@ -148,15 +155,16 @@ function cssModuleJsxStaticComputedUseSites(classNameBindings, prop) {
148
155
  });
149
156
  }
150
157
 
151
- function cssModuleJsxHelperUseSites(classNameBindings, prop) {
158
+ function cssModuleJsxHelperUseSites(classNameBindings, prop, helperCalleeEvidence) {
152
159
  const expressionText = prop.propValueExpressionText ?? prop.propValueDynamicText ?? '';
153
160
  if (!cssModuleExpressionHasCall(expressionText)) return [];
154
161
  const useSites = [];
155
162
  for (const binding of classNameBindings) {
163
+ const helperProof = cssModuleClassHelperCallProof(expressionText, binding, prop, helperCalleeEvidence);
156
164
  for (const occurrence of identifierOccurrences(expressionText, binding.localName)) {
157
165
  const access = cssModuleMemberAccess(expressionText, occurrence.end);
158
166
  if (!access || access.status === 'blocked') continue;
159
- if (memberWriteOperation(expressionText, occurrence.start, access.end)) continue;
167
+ if (cssModuleMemberWriteOperation(expressionText, occurrence.start, access.end)) continue;
160
168
  useSites.push(cssModuleUseSiteRecord(binding, {
161
169
  useSiteKind: 'jsx-className-helper',
162
170
  accessKind: access.accessKind,
@@ -167,14 +175,21 @@ function cssModuleJsxHelperUseSites(classNameBindings, prop) {
167
175
  sourceHash: prop.sourceHash,
168
176
  sourceSpan: prop.sourceSpan,
169
177
  jsxPropRecordId: prop.id,
170
- conditionalRuntimePresence: cssModuleHelperArgumentIsConditional(expressionText, occurrence.start) || undefined
178
+ conditionalRuntimePresence: cssModuleHelperArgumentIsConditional(expressionText, occurrence.start) || undefined,
179
+ helperCallProofLevel: helperProof?.proofLevel,
180
+ helperCallGraphHash: helperProof?.helperCallGraphHash,
181
+ helperCalleeName: helperProof?.helperCalleeName,
182
+ helperCalleeRoot: helperProof?.helperCalleeRoot,
183
+ helperCalleeSource: helperProof?.helperCalleeSource,
184
+ helperModuleSpecifier: helperProof?.helperModuleSpecifier,
185
+ helperImportEdgeId: helperProof?.helperImportEdgeId
171
186
  }));
172
187
  }
173
188
  }
174
189
  return useSites;
175
190
  }
176
191
 
177
- function cssModuleJsxBlockers(classNameBindings, prop) {
192
+ function cssModuleJsxBlockers(classNameBindings, prop, helperCalleeEvidence) {
178
193
  if (prop.propValueKind === 'string') {
179
194
  return classNameBindings.map((binding) => cssModulePropBlocker(binding, prop, 'css-module-string-literal-classname-unproved'));
180
195
  }
@@ -183,43 +198,18 @@ function cssModuleJsxBlockers(classNameBindings, prop) {
183
198
  return classNameBindings.flatMap((binding) => {
184
199
  if (!expressionText.includes(binding.localName)) return [];
185
200
  const reasonCodes = [];
186
- if (cssModuleExpressionHasCall(expressionText)) reasonCodes.push('css-module-helper-call-unproved');
201
+ const helperProof = cssModuleClassHelperCallProof(expressionText, binding, prop, helperCalleeEvidence);
202
+ if (cssModuleExpressionHasCall(expressionText) && !helperProof) reasonCodes.push('css-module-helper-call-unproved');
187
203
  if (cssModuleExpressionHasBlockedAccess(expressionText, binding.localName)) reasonCodes.push('css-module-dynamic-member-access-unproved');
188
204
  if (!reasonCodes.length && cssModuleStaticExpressionAccess(expressionText, binding.localName)) return [];
189
205
  if (!reasonCodes.length && prop.propValueDynamicBlockerReasonCode) {
190
- reasonCodes.push(jsxDynamicReason(prop.propValueDynamicBlockerReasonCode));
206
+ const dynamicReason = jsxDynamicReason(prop.propValueDynamicBlockerReasonCode);
207
+ if (dynamicReason !== 'css-module-helper-call-unproved' || !helperProof) reasonCodes.push(dynamicReason);
191
208
  }
192
209
  return [...new Set(reasonCodes)].map((reasonCode) => cssModulePropBlocker(binding, prop, reasonCode));
193
210
  });
194
211
  }
195
212
 
196
- function memberWriteOperation(sourceText, start, end) {
197
- if (hasPrefixUpdateOperator(sourceText, start)) return 'update';
198
- let index = end;
199
- while (index < sourceText.length && /\s/.test(sourceText[index])) index += 1;
200
- if (sourceText.slice(index, index + 2) === '++' || sourceText.slice(index, index + 2) === '--') return 'update';
201
- return assignmentOperatorAt(sourceText, index) ? 'assignment' : undefined;
202
- }
203
-
204
- function assignmentOperatorAt(code, index) {
205
- for (const operator of ['>>>=', '**=', '<<=', '>>=', '&&=', '||=', '??=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '=']) {
206
- if (!code.startsWith(operator, index)) continue;
207
- if (operator === '=' && (code[index + 1] === '=' || code[index + 1] === '>')) return undefined;
208
- return operator;
209
- }
210
- return undefined;
211
- }
212
-
213
- function hasPrefixUpdateOperator(code, tokenStart) {
214
- let index = tokenStart - 1;
215
- while (index >= 0 && /\s/.test(code[index])) index -= 1;
216
- const operator = code.slice(index - 1, index + 1);
217
- if (operator !== '++' && operator !== '--') return false;
218
- let before = index - 2;
219
- while (before >= 0 && /\s/.test(code[before])) before -= 1;
220
- return before < 0 || !/[A-Za-z0-9_$)\]]/.test(code[before]);
221
- }
222
-
223
213
  function destructuringProperty(text) {
224
214
  const value = String(text ?? '').trim();
225
215
  const [rawName, rawLocal] = value.split(':').map((part) => part.trim());
@@ -249,16 +239,6 @@ function splitTopLevelComma(text, baseOffset) {
249
239
  return parts;
250
240
  }
251
241
 
252
- function cssModuleHelperArgumentIsConditional(expressionText, occurrenceStart) {
253
- const prefix = expressionText.slice(0, occurrenceStart);
254
- const suffix = expressionText.slice(occurrenceStart);
255
- const lastComma = prefix.lastIndexOf(',');
256
- const argumentPrefix = prefix.slice(lastComma + 1);
257
- const nextComma = suffix.indexOf(',');
258
- const argumentText = `${argumentPrefix}${nextComma === -1 ? suffix : suffix.slice(0, nextComma)}`;
259
- return /(?:&&|\|\||\?)/.test(argumentText);
260
- }
261
-
262
242
  function jsxDynamicReason(reasonCode) {
263
243
  if (reasonCode.includes('call')) return 'css-module-helper-call-unproved';
264
244
  if (reasonCode.includes('computed')) return 'css-module-dynamic-member-access-unproved';
@@ -35,7 +35,7 @@ function createProjectCssModuleGraphRecords(semanticIndex, imports, importEdges,
35
35
  .map((edge, index) => cssModuleImportBindingRecord(edge, index, documentsById, cssSourcesWithDependencyGraphsByPath)));
36
36
  const bindingsByLocal = groupBindingsByLocal(importBindings);
37
37
  const { useSites: lexicalUseSites, blockers: lexicalBlockers } = cssModuleLexicalUseSites(importBindings, sourceTextsByPath);
38
- const { useSites: jsxUseSites, blockers: jsxBlockers } = cssModuleJsxUseSites(bindingsByLocal, jsxPropRecords);
38
+ const { useSites: jsxUseSites, blockers: jsxBlockers } = cssModuleJsxUseSites(bindingsByLocal, jsxPropRecords, importEdges);
39
39
  const namedImportBlockers = importBindings
40
40
  .filter((binding) => binding.importKind === 'named')
41
41
  .map((binding) => cssModuleNamedExportBlocker(binding));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-compiler",
3
- "version": "0.2.197",
3
+ "version": "0.2.198",
4
4
  "description": "Compiler facade for Frontier Lang source documents and language projection adapters.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",