@shapeshift-labs/frontier-lang-compiler 0.2.143 → 0.2.145
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/js-ts-safe-merge-enum-member-fallback.js +225 -0
- package/dist/js-ts-safe-merge-enum-member-parser.js +185 -0
- package/dist/js-ts-safe-merge-semantic-artifact-ledger.js +1 -0
- package/dist/js-ts-safe-merge-semantic-edit-fallback.js +3 -3
- package/dist/js-ts-safe-merge-source-shape-fallbacks.js +9 -0
- package/dist/js-ts-semantic-merge-member-source.js +11 -2
- package/package.json +1 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { JsTsSafeMergeStatuses } from './js-ts-safe-merge-constants.js';
|
|
2
|
+
import { createJsTsSafeMergeSemanticArtifacts } from './js-ts-safe-merge-semantic-artifacts.js';
|
|
3
|
+
import { uniqueStrings } from './js-ts-safe-merge-context.js';
|
|
4
|
+
import { semanticFallbackChangedExistingDeclarations } from './js-ts-safe-merge-semantic-edit-fallback-utils.js';
|
|
5
|
+
import {
|
|
6
|
+
enumDeclarationsByName,
|
|
7
|
+
sameEnumMemberText,
|
|
8
|
+
sameEnumShell
|
|
9
|
+
} from './js-ts-safe-merge-enum-member-parser.js';
|
|
10
|
+
|
|
11
|
+
function createEnumMemberSemanticFallbackResult(input, topLevelResult, stagedFallback) {
|
|
12
|
+
const currentSourceText = fallbackCurrentSourceText(input, stagedFallback);
|
|
13
|
+
const merge = mergeEnumMemberSources({
|
|
14
|
+
baseSourceText: input.baseSourceText,
|
|
15
|
+
workerSourceText: input.workerSourceText,
|
|
16
|
+
headSourceText: input.headSourceText,
|
|
17
|
+
currentSourceText
|
|
18
|
+
});
|
|
19
|
+
if (!merge.ok || merge.sourceText === currentSourceText) return undefined;
|
|
20
|
+
const resultBase = stagedFallback?.stagedTopLevelResult ?? topLevelResult;
|
|
21
|
+
const language = input.language ?? topLevelResult.language ?? 'typescript';
|
|
22
|
+
const sourcePath = input.sourcePath ?? topLevelResult.sourcePath ?? 'inline.ts';
|
|
23
|
+
const phase = stagedFallback
|
|
24
|
+
? 'staged-top-level-enum-member-semantic-fallback'
|
|
25
|
+
: 'enum-member-semantic-fallback';
|
|
26
|
+
const artifacts = createJsTsSafeMergeSemanticArtifacts({
|
|
27
|
+
...input,
|
|
28
|
+
id: `${String(input.id ?? topLevelResult.id ?? 'js_ts_safe_merge')}_enum_member`,
|
|
29
|
+
language,
|
|
30
|
+
sourcePath,
|
|
31
|
+
headSourceText: currentSourceText,
|
|
32
|
+
headHash: undefined,
|
|
33
|
+
currentSourceHash: undefined
|
|
34
|
+
}, {
|
|
35
|
+
...resultBase,
|
|
36
|
+
id: `${String(input.id ?? resultBase.id ?? 'js_ts_safe_merge')}_enum_member`,
|
|
37
|
+
language,
|
|
38
|
+
sourcePath,
|
|
39
|
+
mergedSourceText: merge.sourceText,
|
|
40
|
+
outputSourceText: merge.sourceText
|
|
41
|
+
});
|
|
42
|
+
if (artifacts.status !== 'verified') return undefined;
|
|
43
|
+
const gates = semanticArtifactGates(artifacts);
|
|
44
|
+
return {
|
|
45
|
+
...resultBase,
|
|
46
|
+
id: String(input.id ?? resultBase.id ?? topLevelResult.id),
|
|
47
|
+
status: JsTsSafeMergeStatuses.merged,
|
|
48
|
+
mergedSourceText: merge.sourceText,
|
|
49
|
+
outputSourceText: merge.sourceText,
|
|
50
|
+
conflicts: [],
|
|
51
|
+
gates,
|
|
52
|
+
admission: {
|
|
53
|
+
status: 'auto-merge-candidate',
|
|
54
|
+
action: 'apply',
|
|
55
|
+
reviewRequired: false,
|
|
56
|
+
autoApplyCandidate: true,
|
|
57
|
+
autoMergeClaim: false,
|
|
58
|
+
semanticEquivalenceClaim: false,
|
|
59
|
+
reasonCodes: []
|
|
60
|
+
},
|
|
61
|
+
summary: {
|
|
62
|
+
...resultBase.summary,
|
|
63
|
+
changedExistingDeclarations: semanticFallbackChangedExistingDeclarations(topLevelResult, resultBase, stagedFallback),
|
|
64
|
+
conflicts: 0,
|
|
65
|
+
gatesPassed: gates.filter((gate) => gate.status === 'passed').length,
|
|
66
|
+
semanticEditOperations: artifacts.script.summary.operations,
|
|
67
|
+
semanticEditAppliedOperations: artifacts.replay.summary.applied,
|
|
68
|
+
semanticEditReplayStatus: artifacts.replay.status,
|
|
69
|
+
enumMemberDeclarations: merge.summary.declarations,
|
|
70
|
+
enumMemberEdits: merge.summary.edits,
|
|
71
|
+
enumMemberAdditions: merge.summary.additions,
|
|
72
|
+
composedPhases: 2
|
|
73
|
+
},
|
|
74
|
+
metadata: {
|
|
75
|
+
...resultBase.metadata,
|
|
76
|
+
composed: {
|
|
77
|
+
phase,
|
|
78
|
+
phases: stagedFallback
|
|
79
|
+
? ['top-level-neutralization', 'top-level-ledger', 'enum-member']
|
|
80
|
+
: ['top-level-ledger', 'enum-member'],
|
|
81
|
+
originalReasonCodes: topLevelResult.admission?.reasonCodes ?? [],
|
|
82
|
+
stagedTopLevelSummary: stagedFallback?.stagedTopLevelResult?.summary,
|
|
83
|
+
neutralization: stagedFallback?.neutralization?.summary,
|
|
84
|
+
enumMemberFallback: merge.summary
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
semanticArtifacts: artifacts
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function fallbackCurrentSourceText(input, stagedFallback) {
|
|
92
|
+
return stagedFallback?.directReplayCurrentSourceText
|
|
93
|
+
?? stagedFallback?.replayCurrentSourceText
|
|
94
|
+
?? input.headSourceText;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function mergeEnumMemberSources(input) {
|
|
98
|
+
if (![input.baseSourceText, input.workerSourceText, input.headSourceText, input.currentSourceText].every(isString)) {
|
|
99
|
+
return blocked('missing-source-text');
|
|
100
|
+
}
|
|
101
|
+
const base = enumDeclarationsByName(input.baseSourceText);
|
|
102
|
+
const worker = enumDeclarationsByName(input.workerSourceText);
|
|
103
|
+
const head = enumDeclarationsByName(input.headSourceText);
|
|
104
|
+
const current = enumDeclarationsByName(input.currentSourceText);
|
|
105
|
+
if ([base, worker, head, current].some((source) => source.reasonCodes.length)) {
|
|
106
|
+
return blocked('enum-member-parse-blocked');
|
|
107
|
+
}
|
|
108
|
+
const edits = [];
|
|
109
|
+
const summary = { declarations: 0, edits: 0, additions: 0 };
|
|
110
|
+
for (const declaration of base.declarations) {
|
|
111
|
+
const workerDeclaration = worker.byName.get(declaration.name);
|
|
112
|
+
const headDeclaration = head.byName.get(declaration.name);
|
|
113
|
+
const currentDeclaration = current.byName.get(declaration.name);
|
|
114
|
+
if (!workerDeclaration || !headDeclaration || !currentDeclaration) continue;
|
|
115
|
+
const merged = mergeEnumDeclaration(declaration, workerDeclaration, headDeclaration, currentDeclaration);
|
|
116
|
+
if (merged.status === 'blocked') return blocked(...merged.reasonCodes);
|
|
117
|
+
if (!merged.replacement || sameEnumMemberText(merged.replacement, currentDeclaration.text)) continue;
|
|
118
|
+
edits.push({ start: currentDeclaration.start, end: currentDeclaration.end, replacement: merged.replacement });
|
|
119
|
+
summary.declarations += 1;
|
|
120
|
+
summary.edits += 1;
|
|
121
|
+
summary.additions += merged.additions;
|
|
122
|
+
}
|
|
123
|
+
if (!edits.length) return blocked('no-enum-member-merge-candidate');
|
|
124
|
+
const sourceText = edits.sort((left, right) => right.start - left.start)
|
|
125
|
+
.reduce((text, edit) => text.slice(0, edit.start) + edit.replacement + text.slice(edit.end), input.currentSourceText);
|
|
126
|
+
return { ok: true, sourceText, summary };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function mergeEnumDeclaration(base, worker, head, current) {
|
|
130
|
+
if (!sameEnumShell(base, worker) || !sameEnumShell(base, head) || !sameEnumShell(base, current)) {
|
|
131
|
+
return blockedDeclaration('enum-member-shell-changed');
|
|
132
|
+
}
|
|
133
|
+
const baseNames = base.members.map((member) => member.name);
|
|
134
|
+
if (![worker, head, current].every((declaration) => containsBaseMembersInOrder(declaration, baseNames))) {
|
|
135
|
+
return blockedDeclaration('enum-member-base-order-changed');
|
|
136
|
+
}
|
|
137
|
+
const memberMaps = [base, worker, head, current].map(membersByName);
|
|
138
|
+
if (memberMaps.some((map) => map.reasonCodes.length)) return blockedDeclaration('duplicate-enum-member-name');
|
|
139
|
+
const [baseMembers, workerMembers, headMembers, currentMembers] = memberMaps.map((map) => map.byName);
|
|
140
|
+
const mergedByName = new Map();
|
|
141
|
+
for (const name of baseNames) {
|
|
142
|
+
const merged = mergeExistingMember(name, baseMembers, workerMembers, headMembers, currentMembers);
|
|
143
|
+
if (merged.status === 'blocked') return merged;
|
|
144
|
+
mergedByName.set(name, merged.text);
|
|
145
|
+
}
|
|
146
|
+
const workerAdditions = new Map();
|
|
147
|
+
for (const member of worker.members.filter((entry) => !baseMembers.has(entry.name))) {
|
|
148
|
+
const currentMember = currentMembers.get(member.name);
|
|
149
|
+
if (currentMember && !sameEnumMemberText(currentMember.text, member.text)) {
|
|
150
|
+
return blockedDeclaration('enum-member-addition-conflict');
|
|
151
|
+
}
|
|
152
|
+
if (!currentMember) workerAdditions.set(member.name, member.text);
|
|
153
|
+
}
|
|
154
|
+
const outputMembers = [];
|
|
155
|
+
for (const member of current.members) outputMembers.push(mergedByName.get(member.name) ?? member.text);
|
|
156
|
+
for (const text of workerAdditions.values()) outputMembers.push(text);
|
|
157
|
+
const replacement = `${current.prefix}${outputMembers.join(',')}${current.tail}${current.suffix}`;
|
|
158
|
+
return {
|
|
159
|
+
status: 'merged',
|
|
160
|
+
replacement,
|
|
161
|
+
additions: workerAdditions.size
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function mergeExistingMember(name, baseMembers, workerMembers, headMembers, currentMembers) {
|
|
166
|
+
const base = baseMembers.get(name);
|
|
167
|
+
const worker = workerMembers.get(name);
|
|
168
|
+
const head = headMembers.get(name);
|
|
169
|
+
const current = currentMembers.get(name);
|
|
170
|
+
if (!base || !worker || !head || !current) return blockedDeclaration('enum-member-removed');
|
|
171
|
+
const workerChanged = !sameEnumMemberText(base.text, worker.text);
|
|
172
|
+
const headChanged = !sameEnumMemberText(base.text, head.text);
|
|
173
|
+
if (workerChanged && headChanged && !sameEnumMemberText(worker.text, head.text)) {
|
|
174
|
+
return blockedDeclaration('enum-member-conflict');
|
|
175
|
+
}
|
|
176
|
+
if (!sameEnumMemberText(current.text, head.text) && !sameEnumMemberText(current.text, worker.text)) {
|
|
177
|
+
return blockedDeclaration('enum-member-current-diverged');
|
|
178
|
+
}
|
|
179
|
+
return { status: 'merged', text: workerChanged ? worker.text : current.text };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function containsBaseMembersInOrder(declaration, baseNames) {
|
|
183
|
+
let index = 0;
|
|
184
|
+
for (const member of declaration.members) {
|
|
185
|
+
if (member.name === baseNames[index]) index += 1;
|
|
186
|
+
}
|
|
187
|
+
return index === baseNames.length;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function membersByName(declaration) {
|
|
191
|
+
const byName = new Map();
|
|
192
|
+
const reasonCodes = [];
|
|
193
|
+
for (const member of declaration.members) {
|
|
194
|
+
if (byName.has(member.name)) reasonCodes.push('duplicate-enum-member-name');
|
|
195
|
+
byName.set(member.name, member);
|
|
196
|
+
}
|
|
197
|
+
return { byName, reasonCodes: uniqueStrings(reasonCodes) };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function semanticArtifactGates(artifacts) {
|
|
201
|
+
return [
|
|
202
|
+
gate('semantic-edit-script', artifacts.script?.admission?.status === 'auto-merge-candidate', artifacts.script?.admission?.reasonCodes),
|
|
203
|
+
gate('semantic-edit-projection', artifacts.projection?.status === 'projected', artifacts.projection?.admission?.reasonCodes),
|
|
204
|
+
gate('semantic-edit-replay', artifacts.replay?.status === 'accepted-clean', artifacts.replay?.admission?.reasonCodes),
|
|
205
|
+
gate('semantic-edit-already-applied', artifacts.alreadyAppliedReplay?.status === 'already-applied', artifacts.alreadyAppliedReplay?.admission?.reasonCodes)
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function gate(id, passed, reasonCodes = []) {
|
|
210
|
+
return { id, status: passed ? 'passed' : 'blocked', reasonCodes: passed ? [] : uniqueStrings(reasonCodes) };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function blocked(...reasonCodes) {
|
|
214
|
+
return { ok: false, reasonCodes: uniqueStrings(reasonCodes) };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function blockedDeclaration(...reasonCodes) {
|
|
218
|
+
return { status: 'blocked', reasonCodes: uniqueStrings(reasonCodes) };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isString(value) {
|
|
222
|
+
return typeof value === 'string';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export { createEnumMemberSemanticFallbackResult };
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { normalizeLineEndings, uniqueStrings } from './js-ts-safe-merge-context.js';
|
|
2
|
+
|
|
3
|
+
function enumDeclarationsByName(sourceText) {
|
|
4
|
+
const reasonCodes = [];
|
|
5
|
+
const declarations = topLevelEnumDeclarations(sourceText)
|
|
6
|
+
.map((declaration) => parseEnumDeclaration(sourceText, declaration))
|
|
7
|
+
.filter(Boolean);
|
|
8
|
+
const byName = new Map();
|
|
9
|
+
for (const declaration of declarations) {
|
|
10
|
+
if (byName.has(declaration.name)) reasonCodes.push('duplicate-enum-declaration-name');
|
|
11
|
+
byName.set(declaration.name, declaration);
|
|
12
|
+
}
|
|
13
|
+
return { declarations, byName, reasonCodes: uniqueStrings(reasonCodes) };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseEnumDeclaration(sourceText, match) {
|
|
17
|
+
const header = sourceText.slice(match.start, match.bodyStart);
|
|
18
|
+
const nameMatch = /\benum\s+([A-Za-z_$][\w$]*)\s*$/u.exec(header);
|
|
19
|
+
if (!nameMatch) return undefined;
|
|
20
|
+
const body = sourceText.slice(match.bodyStart + 1, match.bodyEnd);
|
|
21
|
+
const members = splitEnumMembers(body, match.bodyStart + 1);
|
|
22
|
+
if (!members.length || members.some((member) => !member.name)) return undefined;
|
|
23
|
+
const tailStart = members.at(-1).end;
|
|
24
|
+
return {
|
|
25
|
+
name: nameMatch[1],
|
|
26
|
+
start: match.start,
|
|
27
|
+
end: match.end,
|
|
28
|
+
text: sourceText.slice(match.start, match.end),
|
|
29
|
+
prefix: sourceText.slice(match.start, match.bodyStart + 1),
|
|
30
|
+
tail: sourceText.slice(tailStart, match.bodyEnd),
|
|
31
|
+
suffix: sourceText.slice(match.bodyEnd, match.end),
|
|
32
|
+
members
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function topLevelEnumDeclarations(sourceText) {
|
|
37
|
+
const declarations = [];
|
|
38
|
+
let state = 'code';
|
|
39
|
+
let depth = 0;
|
|
40
|
+
for (let index = 0; index < sourceText.length; index += 1) {
|
|
41
|
+
const nextIndex = advanceState(sourceText, index, state);
|
|
42
|
+
if (nextIndex.state !== state || nextIndex.index !== index) {
|
|
43
|
+
state = nextIndex.state;
|
|
44
|
+
index = nextIndex.index;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (state !== 'code') continue;
|
|
48
|
+
const char = sourceText[index];
|
|
49
|
+
if (char === '(' || char === '[' || char === '{') depth += 1;
|
|
50
|
+
else if (char === ')' || char === ']' || char === '}') depth -= 1;
|
|
51
|
+
if (depth !== 0 || !wordAt(sourceText, index, 'enum')) continue;
|
|
52
|
+
const bodyStart = sourceText.indexOf('{', index);
|
|
53
|
+
if (bodyStart === -1) continue;
|
|
54
|
+
const bodyEnd = matchingBrace(sourceText, bodyStart);
|
|
55
|
+
if (bodyEnd === -1) continue;
|
|
56
|
+
declarations.push({
|
|
57
|
+
start: enumDeclarationStart(sourceText, index),
|
|
58
|
+
bodyStart,
|
|
59
|
+
bodyEnd,
|
|
60
|
+
end: enumDeclarationEnd(sourceText, bodyEnd + 1)
|
|
61
|
+
});
|
|
62
|
+
index = bodyEnd;
|
|
63
|
+
}
|
|
64
|
+
return declarations;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function splitEnumMembers(body, offset) {
|
|
68
|
+
const members = [];
|
|
69
|
+
let start = 0;
|
|
70
|
+
let state = 'code';
|
|
71
|
+
let depth = 0;
|
|
72
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
73
|
+
const nextIndex = advanceState(body, index, state);
|
|
74
|
+
if (nextIndex.state !== state || nextIndex.index !== index) {
|
|
75
|
+
state = nextIndex.state;
|
|
76
|
+
index = nextIndex.index;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (state !== 'code') continue;
|
|
80
|
+
const char = body[index];
|
|
81
|
+
if (char === '(' || char === '[' || char === '{') depth += 1;
|
|
82
|
+
else if (char === ')' || char === ']' || char === '}') depth -= 1;
|
|
83
|
+
else if (char === ',' && depth === 0) {
|
|
84
|
+
pushEnumMember(members, body, start, index, offset);
|
|
85
|
+
start = index + 1;
|
|
86
|
+
}
|
|
87
|
+
if (depth < 0) return [];
|
|
88
|
+
}
|
|
89
|
+
if (state !== 'code' || depth !== 0) return [];
|
|
90
|
+
pushEnumMember(members, body, start, body.length, offset);
|
|
91
|
+
return members;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function pushEnumMember(members, body, start, end, offset) {
|
|
95
|
+
let localStart = start;
|
|
96
|
+
let localEnd = end;
|
|
97
|
+
while (localStart < localEnd && /\s/u.test(body[localStart])) localStart += 1;
|
|
98
|
+
while (localEnd > localStart && /\s/u.test(body[localEnd - 1])) localEnd -= 1;
|
|
99
|
+
if (localStart === localEnd) return;
|
|
100
|
+
const text = body.slice(start, localEnd);
|
|
101
|
+
const trimmed = body.slice(localStart, localEnd);
|
|
102
|
+
const name = enumMemberName(trimmed);
|
|
103
|
+
members.push({ name, text, start: offset + start, end: offset + localEnd });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function enumMemberName(text) {
|
|
107
|
+
const identifier = /^[A-Za-z_$][\w$]*/u.exec(text)?.[0];
|
|
108
|
+
if (identifier) return identifier;
|
|
109
|
+
const quoted = /^(["'])(.*?)\1/u.exec(text);
|
|
110
|
+
return quoted ? quoted[2] : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function matchingBrace(sourceText, bodyStart) {
|
|
114
|
+
let state = 'code';
|
|
115
|
+
let depth = 0;
|
|
116
|
+
for (let index = bodyStart; index < sourceText.length; index += 1) {
|
|
117
|
+
const nextIndex = advanceState(sourceText, index, state);
|
|
118
|
+
if (nextIndex.state !== state || nextIndex.index !== index) {
|
|
119
|
+
state = nextIndex.state;
|
|
120
|
+
index = nextIndex.index;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (state !== 'code') continue;
|
|
124
|
+
if (sourceText[index] === '{') depth += 1;
|
|
125
|
+
else if (sourceText[index] === '}') {
|
|
126
|
+
depth -= 1;
|
|
127
|
+
if (depth === 0) return index;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return -1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function enumDeclarationStart(sourceText, enumOffset) {
|
|
134
|
+
let start = sourceText.lastIndexOf('\n', enumOffset) + 1;
|
|
135
|
+
const leading = sourceText.slice(start, enumOffset);
|
|
136
|
+
if (/\b(?:export|declare|const)\s+$/u.test(leading)) return start;
|
|
137
|
+
const exportIndex = leading.search(/\bexport\b/u);
|
|
138
|
+
return exportIndex === -1 ? enumOffset : start + exportIndex;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function enumDeclarationEnd(sourceText, bodyCloseEnd) {
|
|
142
|
+
let end = bodyCloseEnd;
|
|
143
|
+
while (end < sourceText.length && /[ \t]/u.test(sourceText[end])) end += 1;
|
|
144
|
+
if (sourceText[end] === ';') end += 1;
|
|
145
|
+
if (sourceText[end] === '\r' && sourceText[end + 1] === '\n') return end + 2;
|
|
146
|
+
if (sourceText[end] === '\n') return end + 1;
|
|
147
|
+
return end;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function advanceState(sourceText, index, state) {
|
|
151
|
+
const char = sourceText[index];
|
|
152
|
+
const next = sourceText[index + 1];
|
|
153
|
+
if (state === 'line-comment') return char === '\n' ? { state: 'code', index } : { state, index };
|
|
154
|
+
if (state === 'block-comment') {
|
|
155
|
+
return char === '*' && next === '/' ? { state: 'code', index: index + 1 } : { state, index };
|
|
156
|
+
}
|
|
157
|
+
if (state !== 'code') {
|
|
158
|
+
if (char === '\\') return { state, index: index + 1 };
|
|
159
|
+
return char === state ? { state: 'code', index } : { state, index };
|
|
160
|
+
}
|
|
161
|
+
if (char === '/' && next === '/') return { state: 'line-comment', index: index + 1 };
|
|
162
|
+
if (char === '/' && next === '*') return { state: 'block-comment', index: index + 1 };
|
|
163
|
+
if (char === '"' || char === "'" || char === '`') return { state: char, index };
|
|
164
|
+
return { state, index };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function wordAt(sourceText, index, word) {
|
|
168
|
+
return sourceText.slice(index, index + word.length) === word
|
|
169
|
+
&& !/[A-Za-z0-9_$]/u.test(sourceText[index - 1] ?? '')
|
|
170
|
+
&& !/[A-Za-z0-9_$]/u.test(sourceText[index + word.length] ?? '');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function sameEnumMemberText(left, right) {
|
|
174
|
+
return normalizeLineEndings(String(left ?? '').trim(), '\n') === normalizeLineEndings(String(right ?? '').trim(), '\n');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function sameEnumShell(left, right) {
|
|
178
|
+
return sameEnumMemberText(left.prefix, right.prefix) && sameEnumMemberText(left.suffix, right.suffix);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export {
|
|
182
|
+
enumDeclarationsByName,
|
|
183
|
+
sameEnumMemberText,
|
|
184
|
+
sameEnumShell
|
|
185
|
+
};
|
|
@@ -174,6 +174,7 @@ function entryName(entry) {
|
|
|
174
174
|
|
|
175
175
|
function entrySymbolKind(entry) {
|
|
176
176
|
if (entry.kind === 'import') return 'import';
|
|
177
|
+
if (entry.declarationInfo?.declarationKind === 'enum') return 'type';
|
|
177
178
|
return entry.declarationInfo?.declarationKind ?? entry.kind;
|
|
178
179
|
}
|
|
179
180
|
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
createStagedDeclarationReplayRecord
|
|
16
16
|
} from './js-ts-safe-merge-staged-declaration-replay.js';
|
|
17
17
|
import { createStagedTopLevelSemanticFallback } from './js-ts-safe-merge-staged-top-level-fallback.js';
|
|
18
|
-
import {
|
|
18
|
+
import { createSourceShapeSemanticFallbackResult } from './js-ts-safe-merge-source-shape-fallbacks.js';
|
|
19
19
|
import { idFragment, uniqueStrings } from './native-import-utils.js';
|
|
20
20
|
|
|
21
21
|
function semanticEditFallbackResult(input, topLevelResult) {
|
|
@@ -31,8 +31,8 @@ function semanticEditFallbackResult(input, topLevelResult) {
|
|
|
31
31
|
artifacts = nextArtifacts.status === 'verified' ? nextArtifacts : artifacts;
|
|
32
32
|
}
|
|
33
33
|
if (artifacts.status !== 'verified') {
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
34
|
+
const sourceShapeResult = createSourceShapeSemanticFallbackResult(input, topLevelResult, stagedFallback);
|
|
35
|
+
if (sourceShapeResult) return sourceShapeResult;
|
|
36
36
|
return semanticEditFallbackBlockedResult(input, topLevelResult, artifacts);
|
|
37
37
|
}
|
|
38
38
|
const resultBase = selectedFallback?.stagedTopLevelResult ?? topLevelResult;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createEnumMemberSemanticFallbackResult } from './js-ts-safe-merge-enum-member-fallback.js';
|
|
2
|
+
import { createVariableDeclaratorSemanticFallbackResult } from './js-ts-safe-merge-variable-declarator-fallback.js';
|
|
3
|
+
|
|
4
|
+
function createSourceShapeSemanticFallbackResult(input, topLevelResult, stagedFallback) {
|
|
5
|
+
return createVariableDeclaratorSemanticFallbackResult(input, topLevelResult, stagedFallback)
|
|
6
|
+
?? createEnumMemberSemanticFallbackResult(input, topLevelResult, stagedFallback);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { createSourceShapeSemanticFallbackResult };
|
|
@@ -17,7 +17,7 @@ function removePreparedMemberAdditions(sourceText, preparedRegions, side) {
|
|
|
17
17
|
const replacements = preparedRegions
|
|
18
18
|
.map((region) => ({
|
|
19
19
|
range: region[side],
|
|
20
|
-
replacement: removeMembersFromBody(region[side].body, region[`${side}AddedMembers`] ?? [], region.kind)
|
|
20
|
+
replacement: removeMembersFromBody(region[side].body, region[`${side}AddedMembers`] ?? [], region.kind, region.base.body)
|
|
21
21
|
}))
|
|
22
22
|
.sort((left, right) => right.range.bodyStart - left.range.bodyStart);
|
|
23
23
|
for (const { range, replacement } of replacements) {
|
|
@@ -88,16 +88,25 @@ function appendReadyBody(before, kind) {
|
|
|
88
88
|
return `${before.replace(/\s*$/, '')},`;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
function removeMembersFromBody(body, members, kind) {
|
|
91
|
+
function removeMembersFromBody(body, members, kind, baseBody) {
|
|
92
92
|
let output = body;
|
|
93
93
|
for (const member of [...members].sort((left, right) => right.start - left.start || right.end - left.end)) {
|
|
94
94
|
output = `${output.slice(0, member.start)}${output.slice(member.end)}`;
|
|
95
95
|
}
|
|
96
96
|
if (kind !== 'object') return output;
|
|
97
|
+
if (typeof baseBody === 'string' && sameObjectBodyIgnoringTrailingDelimiter(output, baseBody)) return baseBody;
|
|
97
98
|
const trailing = String(body ?? '').match(/\s*$/)?.[0] ?? '';
|
|
98
99
|
return output.replace(/,\s*$/, trailing);
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
function sameObjectBodyIgnoringTrailingDelimiter(left, right) {
|
|
103
|
+
return normalizeObjectTrailingDelimiter(left) === normalizeObjectTrailingDelimiter(right);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeObjectTrailingDelimiter(body) {
|
|
107
|
+
return String(body ?? '').replace(/,\s*$/, (trailing) => trailing.replace(',', ''));
|
|
108
|
+
}
|
|
109
|
+
|
|
101
110
|
function minimumIndent(lines) {
|
|
102
111
|
const indents = lines.filter((line) => line.trim()).map(leadingWhitespace);
|
|
103
112
|
return indents.length ? Math.min(...indents) : 0;
|
package/package.json
CHANGED