@shapeshift-labs/frontier-lang-compiler 0.2.162 → 0.2.164
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/dist/internal/index-impl/moduleHostResourceImportMetadata.js +47 -0
- package/dist/internal/index-impl/projectSymbolGraphCompilerAdvancedTypeMetadata.js +84 -12
- package/dist/internal/index-impl/projectSymbolGraphCompilerFacts.js +1 -1
- package/dist/internal/index-impl/projectSymbolGraphCssModules.js +48 -4
- package/dist/internal/index-impl/projectSymbolGraphJsxPropRecordFields.js +91 -0
- package/dist/internal/index-impl/projectSymbolGraphJsxPropValues.js +26 -135
- package/dist/internal/index-impl/projectSymbolGraphJsxRecords.js +4 -31
- package/dist/internal/index-impl/projectSymbolGraphJsxRenderReturnCollectionHelpers.js +201 -0
- package/dist/internal/index-impl/projectSymbolGraphJsxRenderReturnCollections.js +210 -0
- package/dist/internal/index-impl/projectSymbolGraphJsxRenderReturns.js +12 -5
- package/dist/internal/index-impl/projectSymbolGraphJsxSpreadPropValues.js +196 -0
- package/dist/internal/index-impl/projectSymbolGraphJsxStaticLiterals.js +207 -0
- package/dist/internal/index-impl/projectSymbolGraphModuleResolution.js +12 -14
- package/dist/internal/index-impl/projectSymbolGraphPackageConditions.js +22 -17
- package/dist/internal/index-impl/syntaxModuleDeclarationEntries.js +27 -1
- package/dist/js-ts-safe-project-merge-html-css-matrix.js +7 -2
- package/dist/js-ts-safe-project-merge-html-css-summary.js +90 -8
- package/dist/js-ts-safe-project-merge-html-css.js +137 -3
- package/dist/js-ts-safe-project-merge-jsx-graph-conflict-details.js +9 -0
- package/dist/js-ts-safe-project-merge-missing-evidence.js +12 -12
- package/dist/js-ts-safe-project-merge-source-text-candidate.js +68 -0
- package/dist/js-ts-safe-project-merge-summary.js +8 -11
- package/dist/js-ts-safe-project-merge.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
function staticMapCall(expressionText) {
|
|
2
|
+
const value = normalizedReturnExpression(expressionText);
|
|
3
|
+
const target = /^([A-Za-z_$][\w$]*)\s*\.\s*map\s*\(/.exec(value);
|
|
4
|
+
if (!target) return undefined;
|
|
5
|
+
const argsStart = target[0].length - 1;
|
|
6
|
+
const argsEnd = matchingClose(value, argsStart, '(', ')');
|
|
7
|
+
if (argsEnd !== value.length - 1) return undefined;
|
|
8
|
+
return {
|
|
9
|
+
arrayName: target[1],
|
|
10
|
+
callbackText: normalizedText(value.slice(argsStart + 1, argsEnd))
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function staticMapCallback(callbackText) {
|
|
15
|
+
const value = normalizedText(callbackText);
|
|
16
|
+
const match = /^(?:\(\s*([A-Za-z_$][\w$]*)(?:\s*,\s*([A-Za-z_$][\w$]*))?\s*\)|([A-Za-z_$][\w$]*))\s*=>\s*([\s\S]*)$/.exec(value);
|
|
17
|
+
if (!match) return undefined;
|
|
18
|
+
const bodyExpressionText = normalizedReturnExpression(match[4]);
|
|
19
|
+
if (bodyExpressionText.startsWith('{')) return undefined;
|
|
20
|
+
return {
|
|
21
|
+
callbackText: value,
|
|
22
|
+
parameterName: match[1] ?? match[3],
|
|
23
|
+
indexParameterName: match[2],
|
|
24
|
+
bodyExpressionText
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function constArrayLiteralBinding(sourceText, arrayName) {
|
|
29
|
+
const value = String(sourceText ?? '');
|
|
30
|
+
const declaration = new RegExp(`\\bconst\\s+${escapeRegExp(arrayName)}\\s*=\\s*\\[`, 'g').exec(value);
|
|
31
|
+
if (!declaration) return undefined;
|
|
32
|
+
const arrayStart = value.indexOf('[', declaration.index);
|
|
33
|
+
const arrayEnd = matchingClose(value, arrayStart, '[', ']');
|
|
34
|
+
if (arrayEnd < 0) return undefined;
|
|
35
|
+
return { arrayLiteralText: normalizedText(value.slice(arrayStart, arrayEnd + 1)) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function staticMapSourceItemRecord(sourceItemExpressionText) {
|
|
39
|
+
const literal = staticLiteralValue(sourceItemExpressionText);
|
|
40
|
+
if (literal) return { kind: literal.kind, props: new Map() };
|
|
41
|
+
const props = staticObjectLiteralProps(sourceItemExpressionText);
|
|
42
|
+
return props ? { kind: 'object-literal', props } : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function staticObjectLiteralProps(sourceItemExpressionText) {
|
|
46
|
+
const value = normalizedReturnExpression(sourceItemExpressionText);
|
|
47
|
+
if (!value.startsWith('{') || !value.endsWith('}')) return undefined;
|
|
48
|
+
const parts = splitTopLevel(value.slice(1, -1), ',').filter((part) => normalizedText(part));
|
|
49
|
+
if (!parts.length) return undefined;
|
|
50
|
+
const props = new Map();
|
|
51
|
+
for (const part of parts) {
|
|
52
|
+
const match = /^\s*([A-Za-z_$][\w$]*)\s*:\s*([\s\S]*?)\s*$/.exec(part);
|
|
53
|
+
if (!match) return undefined;
|
|
54
|
+
const literal = staticLiteralValue(match[2]);
|
|
55
|
+
if (!literal) return undefined;
|
|
56
|
+
props.set(match[1], literal);
|
|
57
|
+
}
|
|
58
|
+
return props;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function staticLiteralValue(text) {
|
|
62
|
+
const value = normalizedText(text);
|
|
63
|
+
const string = /^"([^"]*)"|'([^']*)'$/.exec(value);
|
|
64
|
+
if (string) return { value: string[1] ?? string[2] ?? '', text: value, kind: 'string' };
|
|
65
|
+
if (/^-?\d+(?:\.\d+)?$/.test(value)) return { value: Number(value), text: value, kind: 'number' };
|
|
66
|
+
if (value === 'true' || value === 'false') return { value: value === 'true', text: value, kind: 'boolean' };
|
|
67
|
+
if (value === 'null') return { value: null, text: value, kind: 'null' };
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function mapKeyResolution(keyEvidence, parameterName, sourceItemProps) {
|
|
72
|
+
if (!keyEvidence) return undefined;
|
|
73
|
+
if (keyEvidence.keyValue !== undefined) return { keyValue: keyEvidence.keyValue, keyStatic: true };
|
|
74
|
+
const prefix = `${parameterName}.`;
|
|
75
|
+
if (!keyEvidence.keyExpressionText?.startsWith(prefix)) return { keyStatic: false };
|
|
76
|
+
const keySourcePropName = keyEvidence.keyExpressionText.slice(prefix.length);
|
|
77
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(keySourcePropName)) return { keyStatic: false };
|
|
78
|
+
const prop = sourceItemProps.get(keySourcePropName);
|
|
79
|
+
return prop ? { keyValue: prop.value, keySourcePropName, keyStatic: true } : { keySourcePropName, keyStatic: false };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function jsxKeyEvidence(expressionText) {
|
|
83
|
+
const openingTag = firstJsxOpeningTagText(expressionText);
|
|
84
|
+
if (!openingTag) return undefined;
|
|
85
|
+
const match = /\bkey\s*=\s*("[^"]*"|'[^']*'|\{[^}]*\})/.exec(openingTag);
|
|
86
|
+
if (!match) return undefined;
|
|
87
|
+
const rawValue = match[1].trim();
|
|
88
|
+
const quoted = /^"([^"]*)"|'([^']*)'$/.exec(rawValue);
|
|
89
|
+
const braced = /^\{\s*([\s\S]*?)\s*\}$/.exec(rawValue);
|
|
90
|
+
return compactRecord({
|
|
91
|
+
keyPropText: normalizedText(match[0]),
|
|
92
|
+
keyValue: quoted ? quoted[1] ?? quoted[2] ?? '' : undefined,
|
|
93
|
+
keyExpressionText: braced ? normalizedText(braced[1]) : undefined
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function firstJsxOpeningTagText(expressionText) {
|
|
98
|
+
const value = normalizedReturnExpression(expressionText);
|
|
99
|
+
const start = value.indexOf('<');
|
|
100
|
+
if (start < 0 || value[start + 1] === '/' || value[start + 1] === '>') return undefined;
|
|
101
|
+
let quote;
|
|
102
|
+
let braceDepth = 0;
|
|
103
|
+
for (let index = start + 1; index < value.length; index += 1) {
|
|
104
|
+
const char = value[index];
|
|
105
|
+
if (quote) {
|
|
106
|
+
if (char === '\\') index += 1;
|
|
107
|
+
else if (char === quote) quote = undefined;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (char === '"' || char === "'" || char === '`') quote = char;
|
|
111
|
+
else if (char === '{') braceDepth += 1;
|
|
112
|
+
else if (char === '}') braceDepth = Math.max(0, braceDepth - 1);
|
|
113
|
+
else if (char === '>' && braceDepth === 0) return value.slice(start, index + 1);
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function directJsxChildren(text) {
|
|
119
|
+
const value = normalizedText(text);
|
|
120
|
+
const children = [];
|
|
121
|
+
const childPattern = /<([A-Za-z][\w.]*)\b[^>]*\/>|<([A-Za-z][\w.]*)\b[^>]*>[\s\S]*?<\/\2>/g;
|
|
122
|
+
for (const match of value.matchAll(childPattern)) children.push(normalizedText(match[0]));
|
|
123
|
+
return children;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isJsxExpression(text) {
|
|
127
|
+
const value = normalizedReturnExpression(text);
|
|
128
|
+
return /^<[A-Za-z][\w.]*(?:\s|>|\/>)/.test(value) || /^React\s*\.\s*createElement\s*\(/.test(value);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function splitTopLevel(text, delimiter) {
|
|
132
|
+
const value = String(text ?? '');
|
|
133
|
+
const parts = [];
|
|
134
|
+
let quote;
|
|
135
|
+
let depth = 0;
|
|
136
|
+
let start = 0;
|
|
137
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
138
|
+
const char = value[index];
|
|
139
|
+
if (quote) {
|
|
140
|
+
if (char === '\\') index += 1;
|
|
141
|
+
else if (char === quote) quote = undefined;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (char === '"' || char === "'" || char === '`') quote = char;
|
|
145
|
+
else if (char === '(' || char === '[' || char === '{' || char === '<') depth += 1;
|
|
146
|
+
else if (char === ')' || char === ']' || char === '}') depth = Math.max(0, depth - 1);
|
|
147
|
+
else if (char === '>' && depth > 0) depth = Math.max(0, depth - 1);
|
|
148
|
+
else if (depth === 0 && value.slice(index, index + delimiter.length) === delimiter) {
|
|
149
|
+
parts.push(value.slice(start, index));
|
|
150
|
+
start = index + delimiter.length;
|
|
151
|
+
index += delimiter.length - 1;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
parts.push(value.slice(start));
|
|
155
|
+
return parts;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function matchingClose(text, openIndex, openChar, closeChar) {
|
|
159
|
+
const value = String(text ?? '');
|
|
160
|
+
let quote;
|
|
161
|
+
let depth = 0;
|
|
162
|
+
for (let index = openIndex; index < value.length; index += 1) {
|
|
163
|
+
const char = value[index];
|
|
164
|
+
if (quote) {
|
|
165
|
+
if (char === '\\') index += 1;
|
|
166
|
+
else if (char === quote) quote = undefined;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (char === '"' || char === "'" || char === '`') quote = char;
|
|
170
|
+
else if (char === openChar) depth += 1;
|
|
171
|
+
else if (char === closeChar) {
|
|
172
|
+
depth -= 1;
|
|
173
|
+
if (depth === 0) return index;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return -1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function escapeRegExp(text) { return String(text ?? '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
180
|
+
function normalizedText(text) { return String(text ?? '').trim().replace(/\s+/g, ' '); }
|
|
181
|
+
function normalizedReturnExpression(text) {
|
|
182
|
+
const value = normalizedText(text);
|
|
183
|
+
const wrapped = /^\(([\s\S]*)\)$/.exec(value);
|
|
184
|
+
return wrapped ? normalizedText(wrapped[1]) : value;
|
|
185
|
+
}
|
|
186
|
+
function compactRecord(record) { return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)); }
|
|
187
|
+
|
|
188
|
+
export {
|
|
189
|
+
compactRecord,
|
|
190
|
+
constArrayLiteralBinding,
|
|
191
|
+
directJsxChildren,
|
|
192
|
+
isJsxExpression,
|
|
193
|
+
jsxKeyEvidence,
|
|
194
|
+
mapKeyResolution,
|
|
195
|
+
normalizedReturnExpression,
|
|
196
|
+
normalizedText,
|
|
197
|
+
splitTopLevel,
|
|
198
|
+
staticMapCall,
|
|
199
|
+
staticMapCallback,
|
|
200
|
+
staticMapSourceItemRecord
|
|
201
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import {
|
|
3
|
+
compactRecord,
|
|
4
|
+
constArrayLiteralBinding,
|
|
5
|
+
directJsxChildren,
|
|
6
|
+
isJsxExpression,
|
|
7
|
+
jsxKeyEvidence,
|
|
8
|
+
mapKeyResolution,
|
|
9
|
+
normalizedReturnExpression,
|
|
10
|
+
splitTopLevel,
|
|
11
|
+
staticMapCall,
|
|
12
|
+
staticMapCallback,
|
|
13
|
+
staticMapSourceItemRecord
|
|
14
|
+
} from './projectSymbolGraphJsxRenderReturnCollectionHelpers.js';
|
|
15
|
+
|
|
16
|
+
function jsxRenderReturnCollectionRecord(expressionText, sourceText) {
|
|
17
|
+
const value = normalizedReturnExpression(expressionText);
|
|
18
|
+
return arrayLiteralCollectionRecord(value)
|
|
19
|
+
?? fragmentCollectionRecord(value)
|
|
20
|
+
?? staticConstArrayMapCollectionRecord(value, sourceText);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function arrayLiteralCollectionRecord(expressionText) {
|
|
24
|
+
if (!expressionText.startsWith('[') || !expressionText.endsWith(']')) return undefined;
|
|
25
|
+
const itemExpressionTexts = splitTopLevel(expressionText.slice(1, -1), ',').map(normalizedReturnExpression).filter(Boolean);
|
|
26
|
+
if (!itemExpressionTexts.length || itemExpressionTexts.some((item) => !isJsxExpression(item))) return undefined;
|
|
27
|
+
return jsxCollectionRecord({
|
|
28
|
+
proofStatus: 'static-render-return-array-evidence',
|
|
29
|
+
reasonCode: 'jsx-render-return-array-static-evidence',
|
|
30
|
+
collectionKind: 'array-literal',
|
|
31
|
+
itemExpressionTexts,
|
|
32
|
+
itemRecords: collectionItemRecords('array-literal', itemExpressionTexts)
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fragmentCollectionRecord(expressionText) {
|
|
37
|
+
if (expressionText.startsWith('<>') && expressionText.endsWith('</>')) {
|
|
38
|
+
const itemExpressionTexts = directJsxChildren(expressionText.slice(2, -3));
|
|
39
|
+
if (!itemExpressionTexts.length) return undefined;
|
|
40
|
+
return jsxCollectionRecord({
|
|
41
|
+
proofStatus: 'static-render-return-fragment-evidence',
|
|
42
|
+
reasonCode: 'jsx-render-return-fragment-static-evidence',
|
|
43
|
+
collectionKind: 'fragment-shorthand',
|
|
44
|
+
itemExpressionTexts,
|
|
45
|
+
itemRecords: collectionItemRecords('fragment-shorthand', itemExpressionTexts)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const named = /^<((?:React\.)?Fragment)\b[^>]*>([\s\S]*)<\/\1>$/.exec(expressionText);
|
|
49
|
+
if (!named) return undefined;
|
|
50
|
+
const itemExpressionTexts = directJsxChildren(named[2]);
|
|
51
|
+
if (!itemExpressionTexts.length) return undefined;
|
|
52
|
+
const collectionKind = named[1] === 'React.Fragment' ? 'fragment-react' : 'fragment-named';
|
|
53
|
+
return jsxCollectionRecord({
|
|
54
|
+
proofStatus: 'static-render-return-fragment-evidence',
|
|
55
|
+
reasonCode: 'jsx-render-return-fragment-static-evidence',
|
|
56
|
+
collectionKind,
|
|
57
|
+
itemExpressionTexts,
|
|
58
|
+
itemRecords: collectionItemRecords(collectionKind, itemExpressionTexts)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function staticConstArrayMapCollectionRecord(expressionText, sourceText) {
|
|
63
|
+
const mapCall = staticMapCall(expressionText);
|
|
64
|
+
if (!mapCall) return undefined;
|
|
65
|
+
const arrayBinding = constArrayLiteralBinding(sourceText, mapCall.arrayName);
|
|
66
|
+
if (!arrayBinding) return undefined;
|
|
67
|
+
const callback = staticMapCallback(mapCall.callbackText);
|
|
68
|
+
if (!callback || !isJsxExpression(callback.bodyExpressionText)) return undefined;
|
|
69
|
+
const sourceItemExpressionTexts = splitTopLevel(arrayBinding.arrayLiteralText.slice(1, -1), ',').map(normalizedReturnExpression).filter(Boolean);
|
|
70
|
+
if (!sourceItemExpressionTexts.length) return undefined;
|
|
71
|
+
const sourceItemRecords = sourceItemExpressionTexts.map(staticMapSourceItemRecord);
|
|
72
|
+
if (sourceItemRecords.some((record) => !record)) return undefined;
|
|
73
|
+
const keyEvidence = jsxKeyEvidence(callback.bodyExpressionText);
|
|
74
|
+
const itemExpressionTexts = sourceItemExpressionTexts.map(() => callback.bodyExpressionText);
|
|
75
|
+
const itemRecords = sourceItemExpressionTexts.map((sourceItemExpressionText, index) => staticConstArrayMapItemRecord({
|
|
76
|
+
callback,
|
|
77
|
+
index,
|
|
78
|
+
keyEvidence,
|
|
79
|
+
sourceItem: sourceItemRecords[index],
|
|
80
|
+
sourceItemExpressionText
|
|
81
|
+
}));
|
|
82
|
+
return jsxCollectionRecord({
|
|
83
|
+
proofStatus: 'static-render-return-map-evidence',
|
|
84
|
+
reasonCode: 'jsx-render-return-static-const-array-map-evidence',
|
|
85
|
+
collectionKind: 'static-const-array-map',
|
|
86
|
+
claimScope: 'static-const-array-map-structure-only',
|
|
87
|
+
renderEquivalenceClaim: false,
|
|
88
|
+
runtimeEquivalenceClaim: false,
|
|
89
|
+
sourceArrayName: mapCall.arrayName,
|
|
90
|
+
sourceArrayItemCount: sourceItemExpressionTexts.length,
|
|
91
|
+
sourceItemExpressionTexts,
|
|
92
|
+
mapCallbackExpressionText: callback.callbackText,
|
|
93
|
+
mapParameterName: callback.parameterName,
|
|
94
|
+
mapIndexParameterName: callback.indexParameterName,
|
|
95
|
+
callbackExpressionText: callback.bodyExpressionText,
|
|
96
|
+
itemExpressionTexts,
|
|
97
|
+
itemRecords
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function staticConstArrayMapItemRecord(input) {
|
|
102
|
+
const { callback, index, keyEvidence, sourceItem, sourceItemExpressionText } = input;
|
|
103
|
+
const keyResolution = mapKeyResolution(keyEvidence, callback.parameterName, sourceItem.props);
|
|
104
|
+
return compactRecord({
|
|
105
|
+
ordinal: index + 1,
|
|
106
|
+
proofStatus: 'static-render-return-map-item-evidence',
|
|
107
|
+
expressionText: callback.bodyExpressionText,
|
|
108
|
+
expressionHash: hashSemanticValue({ kind: 'frontier.lang.projectJsxRenderReturnMapItemExpression', text: callback.bodyExpressionText }),
|
|
109
|
+
sourceItemExpressionText,
|
|
110
|
+
sourceItemExpressionHash: hashSemanticValue({ kind: 'frontier.lang.projectJsxRenderReturnMapSourceItem', text: sourceItemExpressionText }),
|
|
111
|
+
sourceItemKind: sourceItem.kind,
|
|
112
|
+
keyPropText: keyEvidence?.keyPropText,
|
|
113
|
+
keyExpressionText: keyEvidence?.keyExpressionText,
|
|
114
|
+
keyValue: keyResolution?.keyValue,
|
|
115
|
+
keySourcePropName: keyResolution?.keySourcePropName,
|
|
116
|
+
keyStatic: keyResolution?.keyStatic,
|
|
117
|
+
signatureHash: hashSemanticValue({
|
|
118
|
+
kind: 'frontier.lang.projectJsxRenderReturnMapItem',
|
|
119
|
+
ordinal: index + 1,
|
|
120
|
+
expressionText: callback.bodyExpressionText,
|
|
121
|
+
sourceItemExpressionText,
|
|
122
|
+
sourceItemKind: sourceItem.kind,
|
|
123
|
+
keyPropText: keyEvidence?.keyPropText,
|
|
124
|
+
keyExpressionText: keyEvidence?.keyExpressionText,
|
|
125
|
+
keyValue: keyResolution?.keyValue,
|
|
126
|
+
keySourcePropName: keyResolution?.keySourcePropName
|
|
127
|
+
})
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function jsxCollectionRecord(record) {
|
|
132
|
+
const itemRecords = record.itemRecords ?? [];
|
|
133
|
+
const keyedListRecord = keyedListRecordFor(record.collectionKind, itemRecords);
|
|
134
|
+
return compactRecord({
|
|
135
|
+
...record,
|
|
136
|
+
itemCount: itemRecords.length,
|
|
137
|
+
keyedListRecord,
|
|
138
|
+
signatureHash: hashSemanticValue({
|
|
139
|
+
kind: 'frontier.lang.projectJsxRenderReturnCollection',
|
|
140
|
+
proofStatus: record.proofStatus,
|
|
141
|
+
collectionKind: record.collectionKind,
|
|
142
|
+
sourceArrayName: record.sourceArrayName,
|
|
143
|
+
sourceItemExpressionTexts: record.sourceItemExpressionTexts,
|
|
144
|
+
mapCallbackExpressionText: record.mapCallbackExpressionText,
|
|
145
|
+
itemExpressionTexts: record.itemExpressionTexts,
|
|
146
|
+
itemRecords,
|
|
147
|
+
keyedListSignatureHash: keyedListRecord?.signatureHash,
|
|
148
|
+
claimScope: record.claimScope,
|
|
149
|
+
renderEquivalenceClaim: record.renderEquivalenceClaim,
|
|
150
|
+
runtimeEquivalenceClaim: record.runtimeEquivalenceClaim
|
|
151
|
+
})
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function collectionItemRecords(collectionKind, itemExpressionTexts) {
|
|
156
|
+
return itemExpressionTexts.map((itemExpressionText, index) => {
|
|
157
|
+
const key = jsxKeyEvidence(itemExpressionText);
|
|
158
|
+
return compactRecord({
|
|
159
|
+
ordinal: index + 1,
|
|
160
|
+
proofStatus: 'static-render-return-collection-item-evidence',
|
|
161
|
+
expressionText: itemExpressionText,
|
|
162
|
+
expressionHash: hashSemanticValue({ kind: 'frontier.lang.projectJsxRenderReturnCollectionItemExpression', text: itemExpressionText }),
|
|
163
|
+
keyPropText: key?.keyPropText,
|
|
164
|
+
keyExpressionText: key?.keyExpressionText,
|
|
165
|
+
keyValue: key?.keyValue,
|
|
166
|
+
keyStatic: key ? key.keyValue !== undefined : undefined,
|
|
167
|
+
signatureHash: hashSemanticValue({
|
|
168
|
+
kind: 'frontier.lang.projectJsxRenderReturnCollectionItem',
|
|
169
|
+
collectionKind,
|
|
170
|
+
ordinal: index + 1,
|
|
171
|
+
expressionText: itemExpressionText,
|
|
172
|
+
keyPropText: key?.keyPropText,
|
|
173
|
+
keyExpressionText: key?.keyExpressionText,
|
|
174
|
+
keyValue: key?.keyValue
|
|
175
|
+
})
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function keyedListRecordFor(collectionKind, itemRecords) {
|
|
181
|
+
if (!itemRecords.length || itemRecords.some((record) => record.keyStatic === false || (!record.keyPropText && !record.keyExpressionText && record.keyValue === undefined))) return undefined;
|
|
182
|
+
const keyRecords = itemRecords.map((record) => compactRecord({
|
|
183
|
+
ordinal: record.ordinal,
|
|
184
|
+
keyPropText: record.keyPropText,
|
|
185
|
+
keyExpressionText: record.keyExpressionText,
|
|
186
|
+
keyValue: record.keyValue,
|
|
187
|
+
keySourcePropName: record.keySourcePropName,
|
|
188
|
+
keyStatic: record.keyStatic
|
|
189
|
+
}));
|
|
190
|
+
return compactRecord({
|
|
191
|
+
proofStatus: 'static-render-return-keyed-list-evidence',
|
|
192
|
+
reasonCode: 'jsx-render-return-keyed-list-static-evidence',
|
|
193
|
+
claimScope: 'static-list-key-identity-only',
|
|
194
|
+
renderEquivalenceClaim: false,
|
|
195
|
+
runtimeEquivalenceClaim: false,
|
|
196
|
+
keyCount: keyRecords.length,
|
|
197
|
+
keyRecords,
|
|
198
|
+
keyValues: keyRecords.every((record) => record.keyValue !== undefined) ? keyRecords.map((record) => record.keyValue) : undefined,
|
|
199
|
+
signatureHash: hashSemanticValue({
|
|
200
|
+
kind: 'frontier.lang.projectJsxRenderReturnKeyedList',
|
|
201
|
+
collectionKind,
|
|
202
|
+
keyRecords,
|
|
203
|
+
claimScope: 'static-list-key-identity-only',
|
|
204
|
+
renderEquivalenceClaim: false,
|
|
205
|
+
runtimeEquivalenceClaim: false
|
|
206
|
+
})
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { jsxRenderReturnCollectionRecord };
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
-
import { jsxRenderReturnCollectionRecord } from './
|
|
2
|
+
import { jsxRenderReturnCollectionRecord } from './projectSymbolGraphJsxRenderReturnCollections.js';
|
|
3
3
|
|
|
4
4
|
function jsxRenderReturnRecords(sourceText) {
|
|
5
5
|
const statements = returnStatements(sourceText);
|
|
6
6
|
const implicitArrows = statements.length ? [] : implicitArrowReturnStatements(sourceText);
|
|
7
7
|
return [...statements, ...implicitArrows]
|
|
8
|
-
.
|
|
9
|
-
|
|
8
|
+
.map((statement) => ({
|
|
9
|
+
statement,
|
|
10
|
+
collectionRecord: jsxRenderReturnCollectionRecord(statement.expressionText, sourceText)
|
|
11
|
+
}))
|
|
12
|
+
.filter(({ statement, collectionRecord }) => collectionRecord || isRenderableReturnExpression(statement.expressionText))
|
|
13
|
+
.map(({ statement, collectionRecord }, index) => renderReturnRecord(statement, index, collectionRecord));
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
function jsxRenderReturnRiskEvidence(owner) {
|
|
@@ -18,6 +22,8 @@ function jsxRenderReturnRiskEvidence(owner) {
|
|
|
18
22
|
const hasLogicalBranchEvidence = records.some((record) => record.logicalBranchRecord);
|
|
19
23
|
const hasArrayCollectionEvidence = records.some((record) => record.collectionRecord?.collectionKind === 'array-literal');
|
|
20
24
|
const hasFragmentCollectionEvidence = records.some((record) => String(record.collectionRecord?.collectionKind ?? '').startsWith('fragment-'));
|
|
25
|
+
const hasMapCollectionEvidence = records.some((record) => record.collectionRecord?.collectionKind === 'static-const-array-map');
|
|
26
|
+
const hasKeyedListEvidence = records.some((record) => record.collectionRecord?.keyedListRecord);
|
|
21
27
|
const renderRiskKinds = ['render-return-boundary', branched ? 'render-return-branch-control-flow' : undefined].filter(Boolean);
|
|
22
28
|
const hasImplicitArrow = records.some((record) => record.returnKind === 'implicit-arrow-expression');
|
|
23
29
|
const renderRiskReasonCodes = [
|
|
@@ -27,6 +33,8 @@ function jsxRenderReturnRiskEvidence(owner) {
|
|
|
27
33
|
hasLogicalBranchEvidence ? 'jsx-render-return-logical-branch-static-evidence' : undefined,
|
|
28
34
|
hasArrayCollectionEvidence ? 'jsx-render-return-array-static-evidence' : undefined,
|
|
29
35
|
hasFragmentCollectionEvidence ? 'jsx-render-return-fragment-static-evidence' : undefined,
|
|
36
|
+
hasMapCollectionEvidence ? 'jsx-render-return-static-const-array-map-evidence' : undefined,
|
|
37
|
+
hasKeyedListEvidence ? 'jsx-render-return-keyed-list-static-evidence' : undefined,
|
|
30
38
|
branched ? 'jsx-render-return-branch-unsupported' : undefined
|
|
31
39
|
].filter(Boolean);
|
|
32
40
|
const record = compactRecord({
|
|
@@ -80,12 +88,11 @@ function implicitArrowReturnStatements(sourceText) {
|
|
|
80
88
|
return statements;
|
|
81
89
|
}
|
|
82
90
|
|
|
83
|
-
function renderReturnRecord(statement, index) {
|
|
91
|
+
function renderReturnRecord(statement, index, collectionRecord) {
|
|
84
92
|
const expressionText = normalizedText(statement.expressionText);
|
|
85
93
|
const ifConditionText = normalizedText(statement.ifConditionText);
|
|
86
94
|
const conditionalBranch = conditionalBranchRecord(expressionText);
|
|
87
95
|
const logicalBranch = logicalBranchRecord(expressionText);
|
|
88
|
-
const collectionRecord = jsxRenderReturnCollectionRecord(expressionText);
|
|
89
96
|
return compactRecord({
|
|
90
97
|
ordinal: index + 1,
|
|
91
98
|
proofStatus: 'static-render-return-evidence',
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { isJsxSpreadAttribute } from '../../js-ts-safe-merge-jsx-attribute-parser.js';
|
|
2
|
+
import { parseStaticObjectLiteralExpression } from './projectSymbolGraphJsxStaticLiterals.js';
|
|
3
|
+
|
|
4
|
+
function jsxSpreadPropValueEvidence(tag, attribute, context = {}) {
|
|
5
|
+
const expressionText = jsxSpreadExpressionText(attribute);
|
|
6
|
+
const source = staticObjectSpreadSource(expressionText, attribute, context);
|
|
7
|
+
if (!source?.status) return dynamicSpreadPropValueEvidence(expressionText, source?.reasonCode);
|
|
8
|
+
const precedence = staticSpreadPrecedenceEvidence(tag, attribute, source.entries);
|
|
9
|
+
const propNames = uniqueStrings(source.entries.map((entry) => entry.propName));
|
|
10
|
+
return compactRecord({
|
|
11
|
+
proofStatus: 'static-object-spread-jsx-prop-value-evidence',
|
|
12
|
+
reasonCode: 'jsx-render-prop-spread-static-object-evidence',
|
|
13
|
+
valueKind: 'object-spread',
|
|
14
|
+
valueText: source.valueText,
|
|
15
|
+
expressionText,
|
|
16
|
+
staticSpreadSourceKind: source.sourceKind,
|
|
17
|
+
staticSpreadSourceName: source.sourceName,
|
|
18
|
+
staticSpreadPropEntries: source.entries,
|
|
19
|
+
staticSpreadPropNames: propNames,
|
|
20
|
+
staticSpreadPropCount: propNames.length,
|
|
21
|
+
staticSpreadEffectivePropEntries: precedence.effectiveEntries,
|
|
22
|
+
staticSpreadEffectivePropNames: uniqueStrings(precedence.effectiveEntries.map((entry) => entry.propName)),
|
|
23
|
+
staticSpreadExplicitOverridePropNames: precedence.explicitOverridePropNames,
|
|
24
|
+
staticSpreadOverridesExplicitPropNames: precedence.overridesExplicitPropNames,
|
|
25
|
+
staticSpreadDuplicatePropNames: duplicatePropNames(source.entries),
|
|
26
|
+
staticSpreadPrecedenceStatus: precedence.status,
|
|
27
|
+
claimScope: 'static-object-spread-props-only',
|
|
28
|
+
renderEquivalenceClaim: false
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function dynamicSpreadPropValueEvidence(expressionText, reasonCode) {
|
|
33
|
+
return {
|
|
34
|
+
proofStatus: 'dynamic-jsx-prop-spread-unsupported',
|
|
35
|
+
reasonCode: 'jsx-render-prop-spread-unsupported',
|
|
36
|
+
valueKind: 'spread-expression',
|
|
37
|
+
expressionText,
|
|
38
|
+
dynamicText: expressionText,
|
|
39
|
+
dynamicBlockerReasonCode: reasonCode ?? jsxPropSpreadDynamicBlockerReasonCode(expressionText),
|
|
40
|
+
claimScope: 'static-object-spread-props-only',
|
|
41
|
+
renderEquivalenceClaim: false
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function staticObjectSpreadSource(expressionText, attribute, context = {}) {
|
|
46
|
+
const text = normalizedText(expressionText);
|
|
47
|
+
if (!text) return { reasonCode: 'jsx-render-prop-spread-expression-unsupported' };
|
|
48
|
+
if (text.startsWith('{')) {
|
|
49
|
+
const parsed = parseStaticObjectLiteralExpression(text);
|
|
50
|
+
return parsed.value
|
|
51
|
+
? { status: 'static', sourceKind: 'inline-object-literal', valueText: text, entries: parsed.value.entries }
|
|
52
|
+
: { reasonCode: parsed.reasonCode ?? jsxPropSpreadDynamicBlockerReasonCode(text) };
|
|
53
|
+
}
|
|
54
|
+
if (simpleIdentifierExpression(text)) {
|
|
55
|
+
const resolved = sameFileConstObjectSpreadSource(text, attribute, context);
|
|
56
|
+
return resolved.status ? resolved : { reasonCode: resolved.reasonCode ?? 'jsx-render-prop-spread-reference-unsupported' };
|
|
57
|
+
}
|
|
58
|
+
return { reasonCode: jsxPropSpreadDynamicBlockerReasonCode(text) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sameFileConstObjectSpreadSource(name, attribute, context = {}) {
|
|
62
|
+
const sourceText = String(context.sourceText ?? '');
|
|
63
|
+
if (!sourceText) return { reasonCode: 'jsx-render-prop-spread-reference-unsupported' };
|
|
64
|
+
const beforeOffset = Number.isFinite(attribute?.start) ? attribute.start : sourceText.length;
|
|
65
|
+
const declarations = constObjectInitializers(sourceText, name).filter((declaration) => declaration.start < beforeOffset);
|
|
66
|
+
if (declarations.length !== 1) return {
|
|
67
|
+
reasonCode: declarations.length > 1 ? 'jsx-render-prop-spread-ambiguous-const-object-unsupported' : 'jsx-render-prop-spread-reference-unsupported'
|
|
68
|
+
};
|
|
69
|
+
const parsed = parseStaticObjectLiteralExpression(declarations[0].initializerText);
|
|
70
|
+
return parsed.value
|
|
71
|
+
? {
|
|
72
|
+
status: 'static',
|
|
73
|
+
sourceKind: 'same-file-const-object',
|
|
74
|
+
sourceName: name,
|
|
75
|
+
valueText: normalizedText(declarations[0].initializerText),
|
|
76
|
+
entries: parsed.value.entries
|
|
77
|
+
}
|
|
78
|
+
: { reasonCode: parsed.reasonCode ?? 'jsx-render-prop-spread-const-object-dynamic-unsupported' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function constObjectInitializers(sourceText, name) {
|
|
82
|
+
const result = [];
|
|
83
|
+
const pattern = new RegExp(`\\bconst\\s+${escapeRegExp(name)}\\s*=\\s*`, 'g');
|
|
84
|
+
let match;
|
|
85
|
+
while ((match = pattern.exec(sourceText))) {
|
|
86
|
+
const start = match.index;
|
|
87
|
+
const initializerText = readConstInitializerText(sourceText, pattern.lastIndex);
|
|
88
|
+
if (initializerText) result.push({ start, initializerText });
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readConstInitializerText(sourceText, start) {
|
|
94
|
+
let quote;
|
|
95
|
+
let depth = 0;
|
|
96
|
+
for (let index = start; index < sourceText.length; index += 1) {
|
|
97
|
+
const char = sourceText[index];
|
|
98
|
+
if (quote) {
|
|
99
|
+
if (char === '\\') index += 1;
|
|
100
|
+
else if (char === quote) quote = undefined;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (char === '"' || char === "'" || char === '`') quote = char;
|
|
104
|
+
else if (char === '(' || char === '[' || char === '{') depth += 1;
|
|
105
|
+
else if (char === ')' || char === ']' || char === '}') depth = Math.max(0, depth - 1);
|
|
106
|
+
else if (depth === 0 && (char === ';' || char === '\n')) return sourceText.slice(start, index).trim();
|
|
107
|
+
}
|
|
108
|
+
return sourceText.slice(start).trim();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function staticSpreadPrecedenceEvidence(tag, attribute, entries) {
|
|
112
|
+
const index = attributeIndexInTag(tag, attribute);
|
|
113
|
+
const before = namedAttributesBefore(tag, index);
|
|
114
|
+
const after = namedAttributesAfter(tag, index);
|
|
115
|
+
const lastEntries = lastStaticEntriesByProp(entries);
|
|
116
|
+
const explicitOverridePropNames = uniqueStrings(lastEntries.filter((entry) => after.has(entry.propName)).map((entry) => entry.propName));
|
|
117
|
+
const overridesExplicitPropNames = uniqueStrings(lastEntries.filter((entry) => before.has(entry.propName)).map((entry) => entry.propName));
|
|
118
|
+
const effectiveEntries = lastEntries.filter((entry) => !after.has(entry.propName));
|
|
119
|
+
return {
|
|
120
|
+
effectiveEntries,
|
|
121
|
+
explicitOverridePropNames,
|
|
122
|
+
overridesExplicitPropNames,
|
|
123
|
+
status: staticSpreadPrecedenceStatus(explicitOverridePropNames, overridesExplicitPropNames, duplicatePropNames(entries))
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function namedAttributesBefore(tag, index) {
|
|
128
|
+
return new Set((tag.attributes ?? []).slice(0, Math.max(0, index)).filter((candidate) => !isJsxSpreadAttribute(candidate)).map((candidate) => candidate.name));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function namedAttributesAfter(tag, index) {
|
|
132
|
+
return new Set((tag.attributes ?? []).slice(index + 1).filter((candidate) => !isJsxSpreadAttribute(candidate)).map((candidate) => candidate.name));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function staticSpreadPrecedenceStatus(explicitOverridePropNames, overridesExplicitPropNames, duplicateNames) {
|
|
136
|
+
if (explicitOverridePropNames.length && overridesExplicitPropNames.length) return 'static-spread-between-explicit-props';
|
|
137
|
+
if (explicitOverridePropNames.length) return 'static-spread-overridden-by-later-explicit-prop';
|
|
138
|
+
if (overridesExplicitPropNames.length) return 'static-spread-overrides-earlier-explicit-prop';
|
|
139
|
+
if (duplicateNames.length) return 'static-spread-duplicate-prop-last-write-wins';
|
|
140
|
+
return 'static-spread-props-preserved';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function attributeIndexInTag(tag, attribute) {
|
|
144
|
+
const attributes = tag.attributes ?? [];
|
|
145
|
+
const direct = attributes.indexOf(attribute);
|
|
146
|
+
if (direct >= 0) return direct;
|
|
147
|
+
return attributes.findIndex((candidate) => candidate.start === attribute?.start && candidate.end === attribute?.end && candidate.name === attribute?.name);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function lastStaticEntriesByProp(entries) {
|
|
151
|
+
const byProp = new Map();
|
|
152
|
+
for (const entry of entries) byProp.set(entry.propName, entry);
|
|
153
|
+
return [...byProp.values()].sort((left, right) => left.ordinal - right.ordinal);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function duplicatePropNames(entries) {
|
|
157
|
+
const counts = new Map();
|
|
158
|
+
for (const entry of entries) counts.set(entry.propName, (counts.get(entry.propName) ?? 0) + 1);
|
|
159
|
+
return [...counts.entries()].filter(([, count]) => count > 1).map(([name]) => name);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function jsxSpreadExpressionText(attribute) {
|
|
163
|
+
const expressionText = normalizedText(attribute?.expressionText);
|
|
164
|
+
if (expressionText) return expressionText;
|
|
165
|
+
const text = String(attribute?.text ?? '').trim();
|
|
166
|
+
const braced = /^\{\s*\.\.\.\s*([\s\S]*?)\s*\}$/.exec(text);
|
|
167
|
+
if (braced) return normalizedText(braced[1]);
|
|
168
|
+
const unbraced = /^\.\.\.\s*([\s\S]*)$/.exec(text);
|
|
169
|
+
return unbraced ? normalizedText(unbraced[1]) : undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function jsxPropSpreadDynamicBlockerReasonCode(text) {
|
|
173
|
+
const value = normalizedText(text);
|
|
174
|
+
if (/\[[\s\S]*\]/.test(value)) return 'jsx-render-prop-spread-computed-reference-unsupported';
|
|
175
|
+
if (/^`[\s\S]*\$\{[\s\S]*`$/.test(value)) return 'jsx-render-prop-spread-template-interpolation-unsupported';
|
|
176
|
+
if (/\?\.\s*\(/.test(value) || /\b[A-Za-z_$][\w$]*(?:\s*(?:\.|\?\.)\s*[A-Za-z_$][\w$]*)*\s*\(/.test(value)) return 'jsx-render-prop-spread-call-expression-unsupported';
|
|
177
|
+
if (/^\{/.test(value)) return 'jsx-render-prop-spread-dynamic-object-unsupported';
|
|
178
|
+
if (staticPropReference(value)) return 'jsx-render-prop-spread-reference-unsupported';
|
|
179
|
+
return 'jsx-render-prop-spread-expression-unsupported';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function staticPropReference(text) {
|
|
183
|
+
const value = normalizedText(text);
|
|
184
|
+
if (!/^(?:this|[A-Za-z_$][\w$]*)(?:\s*\.\s*[A-Za-z_$][\w$]*)*$/.test(value)) return undefined;
|
|
185
|
+
const normalized = value.replace(/\s+/g, '');
|
|
186
|
+
const path = normalized.split('.');
|
|
187
|
+
return { text: normalized, root: path[0], path };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function simpleIdentifierExpression(text) { return /^[A-Za-z_$][\w$]*$/.test(String(text ?? '').trim()); }
|
|
191
|
+
function escapeRegExp(text) { return String(text ?? '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
192
|
+
function uniqueStrings(values) { return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))]; }
|
|
193
|
+
function normalizedText(text) { return String(text ?? '').trim().replace(/\s+/g, ' '); }
|
|
194
|
+
function compactRecord(record) { return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)); }
|
|
195
|
+
|
|
196
|
+
export { jsxSpreadPropValueEvidence };
|