@shapeshift-labs/frontier-lang-compiler 0.2.134 → 0.2.136
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 +4 -1
- package/dist/js-ts-safe-member-merge.js +10 -8
- package/dist/js-ts-safe-merge-semantic-edit-fallback.js +63 -37
- package/dist/js-ts-safe-merge-staged-declaration-replay.js +200 -0
- package/dist/js-ts-safe-merge-staged-top-level-fallback.js +52 -0
- package/dist/js-ts-safe-merge-top-level-neutralization.js +190 -0
- package/dist/js-ts-semantic-merge-member-source.js +40 -7
- package/dist/js-ts-semantic-merge-member-utils.js +5 -2
- package/dist/js-ts-semantic-merge-parse.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -196,7 +196,10 @@ current head is `accepted-clean`, and replay on the projected source is
|
|
|
196
196
|
`already-applied`. Same-anchor head edits, stale anchors, and non-body conflicts
|
|
197
197
|
remain blocked for review. The same fallback composes with declared unordered
|
|
198
198
|
member-addition regions, so a verified body edit can still merge alongside safe
|
|
199
|
-
interface, type, class, or object member additions.
|
|
199
|
+
interface, type, class, or object member additions. Existing class/object method
|
|
200
|
+
or property body edits inside the declared member region are preserved for
|
|
201
|
+
semantic replay while added members are neutralized; object member additions are
|
|
202
|
+
re-emitted with safe commas when both sides add final properties.
|
|
200
203
|
|
|
201
204
|
Project-level JS/TS safe merges compose the same file-level gates across a
|
|
202
205
|
base/worker/head file set. They preserve head-only files, admit worker-only
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
normalizeKind,
|
|
7
7
|
normalizeMemberText,
|
|
8
8
|
parseMembers,
|
|
9
|
+
removePreparedMemberAdditions,
|
|
9
10
|
uniqueStrings
|
|
10
11
|
} from './js-ts-semantic-merge-parse.js';
|
|
11
12
|
import { mergeResult } from './js-ts-safe-member-merge-result.js';
|
|
@@ -43,7 +44,8 @@ function analyzeJsTsSafeMemberAdditions(input = {}) {
|
|
|
43
44
|
function neutralizeJsTsSafeMemberMergeSources(input = {}) {
|
|
44
45
|
const analysis = analyzeJsTsSafeMemberAdditions({
|
|
45
46
|
...input,
|
|
46
|
-
allowNonPolicySourceChanges: true
|
|
47
|
+
allowNonPolicySourceChanges: true,
|
|
48
|
+
allowExistingMemberChanges: true
|
|
47
49
|
});
|
|
48
50
|
if (analysis.reasonCodes.length) {
|
|
49
51
|
return {
|
|
@@ -56,8 +58,8 @@ function neutralizeJsTsSafeMemberMergeSources(input = {}) {
|
|
|
56
58
|
ok: true,
|
|
57
59
|
analysis,
|
|
58
60
|
baseSourceText: analysis.baseSourceText,
|
|
59
|
-
workerSourceText:
|
|
60
|
-
headSourceText:
|
|
61
|
+
workerSourceText: removePreparedMemberAdditions(analysis.workerSourceText, analysis.preparedRegions, 'worker'),
|
|
62
|
+
headSourceText: removePreparedMemberAdditions(analysis.headSourceText, analysis.preparedRegions, 'head')
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -81,7 +83,7 @@ function prepareJsTsSafeMemberAdditions(input = {}) {
|
|
|
81
83
|
reasonCodes.push(...policyReasons);
|
|
82
84
|
if (policyReasons.length) continue;
|
|
83
85
|
if (typeof baseSourceText !== 'string' || typeof workerSourceText !== 'string' || typeof headSourceText !== 'string') continue;
|
|
84
|
-
const prepared = prepareRegion({ region, baseSourceText, workerSourceText, headSourceText });
|
|
86
|
+
const prepared = prepareRegion({ region, baseSourceText, workerSourceText, headSourceText, allowExistingMemberChanges: input.allowExistingMemberChanges });
|
|
85
87
|
reasonCodes.push(...prepared.reasonCodes);
|
|
86
88
|
if (prepared.ok) preparedRegions.push(prepared.value);
|
|
87
89
|
}
|
|
@@ -163,8 +165,8 @@ function prepareRegion(input) {
|
|
|
163
165
|
const baseByKey = membersByKey(baseMembers.members);
|
|
164
166
|
const workerByKey = membersByKey(workerMembers.members);
|
|
165
167
|
const headByKey = membersByKey(headMembers.members);
|
|
166
|
-
reasonCodes.push(...existingMemberReasons(input.region, 'worker', baseMembers.members, workerMembers.members, workerByKey));
|
|
167
|
-
reasonCodes.push(...existingMemberReasons(input.region, 'head', baseMembers.members, headMembers.members, headByKey));
|
|
168
|
+
reasonCodes.push(...existingMemberReasons(input.region, 'worker', baseMembers.members, workerMembers.members, workerByKey, input.allowExistingMemberChanges));
|
|
169
|
+
reasonCodes.push(...existingMemberReasons(input.region, 'head', baseMembers.members, headMembers.members, headByKey, input.allowExistingMemberChanges));
|
|
168
170
|
const workerAddedKeys = workerMembers.members.map((member) => member.key).filter((key) => !baseByKey.has(key));
|
|
169
171
|
const headAddedKeys = headMembers.members.map((member) => member.key).filter((key) => !baseByKey.has(key));
|
|
170
172
|
reasonCodes.push(...duplicateAddedReasons(input.region, workerAddedKeys, workerByKey, headByKey));
|
|
@@ -212,7 +214,7 @@ function duplicateReasons(region, side, members) {
|
|
|
212
214
|
});
|
|
213
215
|
}
|
|
214
216
|
|
|
215
|
-
function existingMemberReasons(region, side, baseMembers, sideMembers, sideByKey) {
|
|
217
|
+
function existingMemberReasons(region, side, baseMembers, sideMembers, sideByKey, allowExistingMemberChanges) {
|
|
216
218
|
const reasonCodes = [];
|
|
217
219
|
const sideBaseOrder = sideMembers.map((member) => member.key).filter((key) => baseMembers.some((baseMember) => baseMember.key === key));
|
|
218
220
|
const baseOrder = baseMembers.map((member) => member.key);
|
|
@@ -225,7 +227,7 @@ function existingMemberReasons(region, side, baseMembers, sideMembers, sideByKey
|
|
|
225
227
|
reasonCodes.push(regionReason(region, `existing-member-removed:${side}:${baseMember.key}`));
|
|
226
228
|
continue;
|
|
227
229
|
}
|
|
228
|
-
if (normalizeMemberText(sideMember.text) !== normalizeMemberText(baseMember.text)) {
|
|
230
|
+
if (!allowExistingMemberChanges && normalizeMemberText(sideMember.text, normalizeKind(region.kind)) !== normalizeMemberText(baseMember.text, normalizeKind(region.kind))) {
|
|
229
231
|
reasonCodes.push(regionReason(region, `existing-member-changed:${side}:${baseMember.key}`));
|
|
230
232
|
}
|
|
231
233
|
}
|
|
@@ -3,16 +3,25 @@ import { createSemanticEditScript } from './internal/index-impl/semanticEditScri
|
|
|
3
3
|
import { projectSemanticEditScriptToSource } from './internal/index-impl/projectSemanticEditScriptToSource.js';
|
|
4
4
|
import { replaySemanticEditProjection } from './internal/index-impl/replaySemanticEditProjection.js';
|
|
5
5
|
import { JsTsSafeMergeConflictCodes, JsTsSafeMergeStatuses } from './js-ts-safe-merge-constants.js';
|
|
6
|
+
import {
|
|
7
|
+
createStagedDeclarationAlreadyAppliedReplay,
|
|
8
|
+
createStagedDeclarationProjection,
|
|
9
|
+
createStagedDeclarationReplayRecord
|
|
10
|
+
} from './js-ts-safe-merge-staged-declaration-replay.js';
|
|
11
|
+
import { createStagedTopLevelSemanticFallback } from './js-ts-safe-merge-staged-top-level-fallback.js';
|
|
6
12
|
import { idFragment, uniqueStrings } from './native-import-utils.js';
|
|
7
13
|
|
|
8
14
|
function semanticEditFallbackResult(input, topLevelResult) {
|
|
9
15
|
if (!shouldTrySemanticEditFallback(topLevelResult)) return topLevelResult;
|
|
10
|
-
const
|
|
16
|
+
const stagedFallback = createStagedTopLevelSemanticFallback(input, topLevelResult);
|
|
17
|
+
const artifacts = createSemanticEditFallbackArtifacts(input, topLevelResult, stagedFallback);
|
|
11
18
|
if (artifacts.status !== 'verified') return semanticEditFallbackBlockedResult(input, topLevelResult, artifacts);
|
|
19
|
+
const resultBase = stagedFallback?.stagedTopLevelResult ?? topLevelResult;
|
|
12
20
|
const mergedSourceText = artifacts.projection.sourceText;
|
|
13
21
|
const gates = semanticEditGates(artifacts);
|
|
14
22
|
return {
|
|
15
|
-
...
|
|
23
|
+
...resultBase,
|
|
24
|
+
id: String(input.id ?? resultBase.id ?? topLevelResult.id),
|
|
16
25
|
status: JsTsSafeMergeStatuses.merged,
|
|
17
26
|
mergedSourceText,
|
|
18
27
|
outputSourceText: mergedSourceText,
|
|
@@ -28,7 +37,8 @@ function semanticEditFallbackResult(input, topLevelResult) {
|
|
|
28
37
|
reasonCodes: []
|
|
29
38
|
},
|
|
30
39
|
summary: {
|
|
31
|
-
...
|
|
40
|
+
...resultBase.summary,
|
|
41
|
+
changedExistingDeclarations: topLevelResult.summary?.changedExistingDeclarations ?? resultBase.summary?.changedExistingDeclarations ?? 0,
|
|
32
42
|
conflicts: 0,
|
|
33
43
|
gatesPassed: gates.filter((gate) => gate.status === 'passed').length,
|
|
34
44
|
semanticEditOperations: artifacts.script.summary.operations,
|
|
@@ -37,11 +47,13 @@ function semanticEditFallbackResult(input, topLevelResult) {
|
|
|
37
47
|
composedPhases: 2
|
|
38
48
|
},
|
|
39
49
|
metadata: {
|
|
40
|
-
...
|
|
50
|
+
...resultBase.metadata,
|
|
41
51
|
composed: {
|
|
42
|
-
phase: 'semantic-edit-fallback',
|
|
43
|
-
phases: ['top-level-ledger', 'semantic-edit'],
|
|
44
|
-
originalReasonCodes: topLevelResult.admission?.reasonCodes ?? []
|
|
52
|
+
phase: stagedFallback ? 'staged-top-level-semantic-edit-fallback' : 'semantic-edit-fallback',
|
|
53
|
+
phases: stagedFallback ? ['top-level-neutralization', 'top-level-ledger', 'semantic-edit'] : ['top-level-ledger', 'semantic-edit'],
|
|
54
|
+
originalReasonCodes: topLevelResult.admission?.reasonCodes ?? [],
|
|
55
|
+
stagedTopLevelSummary: stagedFallback?.stagedTopLevelResult?.summary,
|
|
56
|
+
neutralization: stagedFallback?.neutralization?.summary
|
|
45
57
|
}
|
|
46
58
|
},
|
|
47
59
|
semanticArtifacts: artifacts
|
|
@@ -53,42 +65,53 @@ function shouldTrySemanticEditFallback(result) {
|
|
|
53
65
|
return conflicts.length > 0 && conflicts.every((conflict) => conflict.code === JsTsSafeMergeConflictCodes.changedExistingDeclaration);
|
|
54
66
|
}
|
|
55
67
|
|
|
56
|
-
function createSemanticEditFallbackArtifacts(input, topLevelResult) {
|
|
68
|
+
function createSemanticEditFallbackArtifacts(input, topLevelResult, stagedFallback) {
|
|
57
69
|
try {
|
|
58
70
|
const id = String(input.id ?? topLevelResult.id ?? 'js_ts_safe_merge');
|
|
59
71
|
const language = input.language ?? topLevelResult.language ?? 'typescript';
|
|
60
72
|
const sourcePath = input.sourcePath ?? topLevelResult.sourcePath ?? 'inline.ts';
|
|
73
|
+
const scriptInput = stagedFallback?.scriptInput ?? input;
|
|
74
|
+
const projectionHeadSourceText = stagedFallback?.projectionHeadSourceText ?? input.headSourceText;
|
|
75
|
+
const replayCurrentSourceText = stagedFallback?.replayCurrentSourceText ?? input.headSourceText;
|
|
61
76
|
const script = createSemanticEditScript({
|
|
62
|
-
...
|
|
77
|
+
...scriptInput,
|
|
63
78
|
id: `${id}_semantic_edit`,
|
|
64
79
|
language,
|
|
65
80
|
sourcePath
|
|
66
81
|
});
|
|
67
|
-
const projection =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
82
|
+
const projection = stagedFallback
|
|
83
|
+
? createStagedDeclarationProjection({ id, script, sourcePath, language, stagedFallback })
|
|
84
|
+
: projectSemanticEditScriptToSource({
|
|
85
|
+
id: `${id}_semantic_edit_projection`,
|
|
86
|
+
script,
|
|
87
|
+
workerSourceText: scriptInput.workerSourceText,
|
|
88
|
+
headSourceText: projectionHeadSourceText,
|
|
89
|
+
headSourcePath: sourcePath,
|
|
90
|
+
parser: input.parser,
|
|
91
|
+
metadata: stagedFallback?.metadata
|
|
92
|
+
});
|
|
93
|
+
const replay = stagedFallback
|
|
94
|
+
? createStagedDeclarationReplayRecord({ id, projection, sourcePath, language, stagedFallback, replayCurrentSourceText })
|
|
95
|
+
: replaySemanticEditProjection({
|
|
96
|
+
id: `${id}_semantic_edit_replay`,
|
|
97
|
+
projection,
|
|
98
|
+
currentSourceText: replayCurrentSourceText,
|
|
99
|
+
currentSourcePath: sourcePath,
|
|
100
|
+
language,
|
|
101
|
+
parser: input.parser,
|
|
102
|
+
metadata: stagedFallback?.metadata
|
|
103
|
+
});
|
|
104
|
+
const alreadyAppliedReplay = stagedFallback
|
|
105
|
+
? createStagedDeclarationAlreadyAppliedReplay({ id, projection, sourcePath, language })
|
|
106
|
+
: replaySemanticEditProjection({
|
|
107
|
+
id: `${id}_semantic_edit_already_applied`,
|
|
108
|
+
projection,
|
|
109
|
+
currentSourceText: projection.sourceText,
|
|
110
|
+
currentSourcePath: sourcePath,
|
|
111
|
+
currentSourceHash: projection.projectedHash,
|
|
112
|
+
language,
|
|
113
|
+
parser: input.parser
|
|
114
|
+
});
|
|
92
115
|
return semanticEditArtifacts({
|
|
93
116
|
id,
|
|
94
117
|
language,
|
|
@@ -97,7 +120,8 @@ function createSemanticEditFallbackArtifacts(input, topLevelResult) {
|
|
|
97
120
|
projection,
|
|
98
121
|
replay,
|
|
99
122
|
alreadyAppliedReplay,
|
|
100
|
-
topLevelResult
|
|
123
|
+
topLevelResult,
|
|
124
|
+
stagedFallback
|
|
101
125
|
});
|
|
102
126
|
} catch (error) {
|
|
103
127
|
return blockedSemanticEditArtifacts(input, topLevelResult, ['semantic-edit-fallback-error'], error);
|
|
@@ -148,8 +172,10 @@ function semanticEditArtifacts(input) {
|
|
|
148
172
|
metadata: {
|
|
149
173
|
autoMergeClaim: false,
|
|
150
174
|
semanticEquivalenceClaim: false,
|
|
151
|
-
source: 'js-ts-semantic-edit-fallback',
|
|
152
|
-
originalReasonCodes: input.topLevelResult.admission?.reasonCodes ?? []
|
|
175
|
+
source: input.stagedFallback ? 'js-ts-staged-top-level-semantic-edit-fallback' : 'js-ts-semantic-edit-fallback',
|
|
176
|
+
originalReasonCodes: input.topLevelResult.admission?.reasonCodes ?? [],
|
|
177
|
+
stagedTopLevelSummary: input.stagedFallback?.stagedTopLevelResult?.summary,
|
|
178
|
+
neutralization: input.stagedFallback?.neutralization?.summary
|
|
153
179
|
}
|
|
154
180
|
};
|
|
155
181
|
return { ...core, hash: hashSemanticValue(core) };
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import { idFragment, uniqueStrings } from './native-import-utils.js';
|
|
3
|
+
|
|
4
|
+
function createStagedDeclarationProjection(input) {
|
|
5
|
+
const replay = input.stagedFallback.declarationReplay;
|
|
6
|
+
const reasonCodes = uniqueStrings(replay.reasonCodes);
|
|
7
|
+
const blocked = reasonCodes.length > 0;
|
|
8
|
+
const sourceText = blocked ? undefined : replay.outputSourceText;
|
|
9
|
+
const edits = blocked ? [] : replay.edits.map((edit, index) => stagedDeclarationProjectionEdit(edit, index, input));
|
|
10
|
+
const core = {
|
|
11
|
+
kind: 'frontier.lang.semanticEditProjection',
|
|
12
|
+
version: 1,
|
|
13
|
+
id: `${input.id}_semantic_edit_projection`,
|
|
14
|
+
scriptId: input.script.id,
|
|
15
|
+
status: blocked ? 'blocked' : 'projected',
|
|
16
|
+
sourcePath: input.sourcePath,
|
|
17
|
+
language: input.language,
|
|
18
|
+
baseHash: input.script.baseHash,
|
|
19
|
+
workerHash: input.script.workerHash,
|
|
20
|
+
headHash: input.script.headHash,
|
|
21
|
+
projectedHash: sourceText === undefined ? undefined : hashSemanticValue(sourceText),
|
|
22
|
+
appliedOperations: edits.map((edit) => edit.operationId),
|
|
23
|
+
skippedOperations: blocked ? (input.script.operations ?? []).map((operation) => operation.id) : [],
|
|
24
|
+
edits,
|
|
25
|
+
sourceText,
|
|
26
|
+
admission: {
|
|
27
|
+
status: blocked ? 'blocked' : 'auto-merge-candidate',
|
|
28
|
+
autoMergeClaim: false,
|
|
29
|
+
semanticEquivalenceClaim: false,
|
|
30
|
+
reasonCodes
|
|
31
|
+
},
|
|
32
|
+
metadata: {
|
|
33
|
+
autoMergeClaim: false,
|
|
34
|
+
semanticEquivalenceClaim: false,
|
|
35
|
+
source: 'js-ts-staged-declaration-replay',
|
|
36
|
+
...input.stagedFallback.metadata
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
return { ...core, hash: hashSemanticValue(core) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stagedDeclarationProjectionEdit(edit, index, input) {
|
|
43
|
+
const operationId = `staged_declaration_replay_${idFragment([input.id, edit.key, index].join(':'))}`;
|
|
44
|
+
const replacementText = edit.replacementText;
|
|
45
|
+
const deletedText = edit.currentText;
|
|
46
|
+
return {
|
|
47
|
+
operationId,
|
|
48
|
+
status: 'applied',
|
|
49
|
+
kind: 'replaceDeclaration',
|
|
50
|
+
editKind: 'replace',
|
|
51
|
+
changeKind: 'modified',
|
|
52
|
+
anchorKey: edit.key,
|
|
53
|
+
conflictKey: `declaration:${edit.key}`,
|
|
54
|
+
regionKind: 'declaration',
|
|
55
|
+
sourcePath: input.sourcePath,
|
|
56
|
+
symbolName: edit.names?.[0],
|
|
57
|
+
symbolKind: edit.declarationKind,
|
|
58
|
+
editContentHash: hashSemanticValue({ operationId, replacementText, deletedText }),
|
|
59
|
+
headStart: edit.start,
|
|
60
|
+
headEnd: edit.end,
|
|
61
|
+
editOrder: index,
|
|
62
|
+
deletedBytes: deletedText.length,
|
|
63
|
+
replacementBytes: replacementText.length,
|
|
64
|
+
deletedTextHash: hashSemanticValue(deletedText),
|
|
65
|
+
replacementTextHash: hashSemanticValue(replacementText),
|
|
66
|
+
replacementText
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createStagedDeclarationReplayRecord(input) {
|
|
71
|
+
const projectionReady = input.projection.status === 'projected';
|
|
72
|
+
const outputSourceText = projectionReady ? input.projection.sourceText : undefined;
|
|
73
|
+
const edits = projectionReady ? input.projection.edits.map((edit) => ({
|
|
74
|
+
operationId: edit.operationId,
|
|
75
|
+
editKind: edit.editKind,
|
|
76
|
+
editOrder: edit.editOrder,
|
|
77
|
+
sourcePath: edit.sourcePath,
|
|
78
|
+
symbolName: edit.symbolName,
|
|
79
|
+
symbolKind: edit.symbolKind,
|
|
80
|
+
status: 'applied',
|
|
81
|
+
start: edit.headStart,
|
|
82
|
+
end: edit.headEnd,
|
|
83
|
+
replacementBytes: edit.replacementBytes,
|
|
84
|
+
replacementText: edit.replacementText,
|
|
85
|
+
reasonCodes: ['staged-declaration-replay']
|
|
86
|
+
})) : [];
|
|
87
|
+
const status = projectionReady ? 'accepted-clean' : 'blocked';
|
|
88
|
+
const reasonCodes = projectionReady ? [] : input.projection.admission.reasonCodes;
|
|
89
|
+
const core = {
|
|
90
|
+
kind: 'frontier.lang.semanticEditReplay',
|
|
91
|
+
version: 1,
|
|
92
|
+
schema: 'frontier.lang.semanticEditReplay.v1',
|
|
93
|
+
id: `${input.id}_semantic_edit_replay`,
|
|
94
|
+
projectionId: input.projection.id,
|
|
95
|
+
scriptId: input.projection.scriptId,
|
|
96
|
+
sourcePath: input.sourcePath,
|
|
97
|
+
language: input.language,
|
|
98
|
+
currentHash: hashSemanticValue(input.replayCurrentSourceText),
|
|
99
|
+
projectedHash: input.projection.projectedHash,
|
|
100
|
+
outputHash: outputSourceText === undefined ? undefined : hashSemanticValue(outputSourceText),
|
|
101
|
+
status,
|
|
102
|
+
edits,
|
|
103
|
+
appliedOperations: edits.map((edit) => edit.operationId),
|
|
104
|
+
skippedOperations: [],
|
|
105
|
+
admission: {
|
|
106
|
+
status,
|
|
107
|
+
action: projectionReady ? 'apply' : 'block',
|
|
108
|
+
reviewRequired: !projectionReady,
|
|
109
|
+
autoApplyCandidate: projectionReady,
|
|
110
|
+
autoMergeClaim: false,
|
|
111
|
+
semanticEquivalenceClaim: false,
|
|
112
|
+
reasonCodes
|
|
113
|
+
},
|
|
114
|
+
outputSourceText,
|
|
115
|
+
summary: {
|
|
116
|
+
edits: edits.length,
|
|
117
|
+
applied: edits.length,
|
|
118
|
+
alreadyApplied: 0,
|
|
119
|
+
conflicts: 0,
|
|
120
|
+
stale: 0,
|
|
121
|
+
blocked: projectionReady ? 0 : 1,
|
|
122
|
+
reasonCodes
|
|
123
|
+
},
|
|
124
|
+
metadata: {
|
|
125
|
+
autoMergeClaim: false,
|
|
126
|
+
semanticEquivalenceClaim: false,
|
|
127
|
+
source: 'js-ts-staged-declaration-replay',
|
|
128
|
+
...input.stagedFallback.metadata
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
return { ...core, hash: hashSemanticValue(core) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function createStagedDeclarationAlreadyAppliedReplay(input) {
|
|
135
|
+
const projectionReady = input.projection.status === 'projected';
|
|
136
|
+
const edits = projectionReady ? input.projection.edits.map((edit) => ({
|
|
137
|
+
operationId: edit.operationId,
|
|
138
|
+
editKind: edit.editKind,
|
|
139
|
+
editOrder: edit.editOrder,
|
|
140
|
+
sourcePath: edit.sourcePath,
|
|
141
|
+
symbolName: edit.symbolName,
|
|
142
|
+
symbolKind: edit.symbolKind,
|
|
143
|
+
status: 'already-applied',
|
|
144
|
+
start: edit.headStart,
|
|
145
|
+
end: edit.headStart + edit.replacementBytes,
|
|
146
|
+
replacementBytes: edit.replacementBytes,
|
|
147
|
+
replacementText: edit.replacementText,
|
|
148
|
+
reasonCodes: ['staged-declaration-already-applied']
|
|
149
|
+
})) : [];
|
|
150
|
+
const status = projectionReady ? 'already-applied' : 'blocked';
|
|
151
|
+
const reasonCodes = projectionReady ? [] : input.projection.admission.reasonCodes;
|
|
152
|
+
const core = {
|
|
153
|
+
kind: 'frontier.lang.semanticEditReplay',
|
|
154
|
+
version: 1,
|
|
155
|
+
schema: 'frontier.lang.semanticEditReplay.v1',
|
|
156
|
+
id: `${input.id}_semantic_edit_already_applied`,
|
|
157
|
+
projectionId: input.projection.id,
|
|
158
|
+
scriptId: input.projection.scriptId,
|
|
159
|
+
sourcePath: input.sourcePath,
|
|
160
|
+
language: input.language,
|
|
161
|
+
currentHash: input.projection.projectedHash,
|
|
162
|
+
projectedHash: input.projection.projectedHash,
|
|
163
|
+
outputHash: input.projection.projectedHash,
|
|
164
|
+
status,
|
|
165
|
+
edits,
|
|
166
|
+
appliedOperations: [],
|
|
167
|
+
skippedOperations: edits.map((edit) => edit.operationId),
|
|
168
|
+
admission: {
|
|
169
|
+
status,
|
|
170
|
+
action: projectionReady ? 'skip' : 'block',
|
|
171
|
+
reviewRequired: !projectionReady,
|
|
172
|
+
autoApplyCandidate: false,
|
|
173
|
+
autoMergeClaim: false,
|
|
174
|
+
semanticEquivalenceClaim: false,
|
|
175
|
+
reasonCodes
|
|
176
|
+
},
|
|
177
|
+
outputSourceText: input.projection.sourceText,
|
|
178
|
+
summary: {
|
|
179
|
+
edits: edits.length,
|
|
180
|
+
applied: 0,
|
|
181
|
+
alreadyApplied: edits.length,
|
|
182
|
+
conflicts: 0,
|
|
183
|
+
stale: 0,
|
|
184
|
+
blocked: projectionReady ? 0 : 1,
|
|
185
|
+
reasonCodes
|
|
186
|
+
},
|
|
187
|
+
metadata: {
|
|
188
|
+
autoMergeClaim: false,
|
|
189
|
+
semanticEquivalenceClaim: false,
|
|
190
|
+
source: 'js-ts-staged-declaration-replay'
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
return { ...core, hash: hashSemanticValue(core) };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export {
|
|
197
|
+
createStagedDeclarationAlreadyAppliedReplay,
|
|
198
|
+
createStagedDeclarationProjection,
|
|
199
|
+
createStagedDeclarationReplayRecord
|
|
200
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { safeMergeJsTsImportsAndDeclarations } from './js-ts-safe-merge.js';
|
|
2
|
+
import { JsTsSafeMergeStatuses } from './js-ts-safe-merge-constants.js';
|
|
3
|
+
import { createJsTsChangedDeclarationReplay, neutralizeJsTsSafeTopLevelMergeSources } from './js-ts-safe-merge-top-level-neutralization.js';
|
|
4
|
+
|
|
5
|
+
function createStagedTopLevelSemanticFallback(input, topLevelResult) {
|
|
6
|
+
const neutralization = neutralizeJsTsSafeTopLevelMergeSources(input);
|
|
7
|
+
if (!neutralization.ok) return undefined;
|
|
8
|
+
const stagedTopLevelResult = safeMergeJsTsImportsAndDeclarations({
|
|
9
|
+
...input,
|
|
10
|
+
baseSourceText: neutralization.baseSourceText,
|
|
11
|
+
workerSourceText: neutralization.topLevelWorkerSourceText,
|
|
12
|
+
headSourceText: neutralization.topLevelHeadSourceText
|
|
13
|
+
});
|
|
14
|
+
if (stagedTopLevelResult.status !== JsTsSafeMergeStatuses.merged) return undefined;
|
|
15
|
+
const safeTopLevelChanges = safeTopLevelChangeCount(stagedTopLevelResult.summary);
|
|
16
|
+
const declarationReplay = createJsTsChangedDeclarationReplay(input, neutralization, stagedTopLevelResult.mergedSourceText);
|
|
17
|
+
const workerDeclarationChanges = neutralization.summary.workerChangedExistingDeclarations ?? 0;
|
|
18
|
+
if (safeTopLevelChanges === 0 && workerDeclarationChanges === 0) return undefined;
|
|
19
|
+
return {
|
|
20
|
+
neutralization,
|
|
21
|
+
declarationReplay,
|
|
22
|
+
stagedTopLevelResult,
|
|
23
|
+
scriptInput: {
|
|
24
|
+
...input,
|
|
25
|
+
workerSourceText: neutralization.semanticWorkerSourceText,
|
|
26
|
+
headSourceText: neutralization.semanticHeadSourceText,
|
|
27
|
+
workerSourceHash: undefined,
|
|
28
|
+
headSourceHash: undefined
|
|
29
|
+
},
|
|
30
|
+
projectionHeadSourceText: stagedTopLevelResult.mergedSourceText,
|
|
31
|
+
replayCurrentSourceText: stagedTopLevelResult.mergedSourceText,
|
|
32
|
+
metadata: {
|
|
33
|
+
stagedTopLevelSummary: stagedTopLevelResult.summary,
|
|
34
|
+
declarationReplay: {
|
|
35
|
+
edits: declarationReplay.edits.length,
|
|
36
|
+
reasonCodes: declarationReplay.reasonCodes
|
|
37
|
+
},
|
|
38
|
+
neutralization: neutralization.summary,
|
|
39
|
+
originalReasonCodes: topLevelResult.admission?.reasonCodes ?? []
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function safeTopLevelChangeCount(summary = {}) {
|
|
45
|
+
return (summary.importSpecifierAdditions ?? 0)
|
|
46
|
+
+ (summary.importDeclarationAdditions ?? 0)
|
|
47
|
+
+ (summary.topLevelDeclarationAdditions ?? 0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
createStagedTopLevelSemanticFallback
|
|
52
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { findCompatibleBaseImportEntry, findSameImportTargetBaseEntry } from './js-ts-safe-merge-import-shape.js';
|
|
2
|
+
import { scanJsTsTopLevelLedger } from './js-ts-safe-merge-ledger.js';
|
|
3
|
+
import { sameStatementText } from './js-ts-safe-merge-context.js';
|
|
4
|
+
|
|
5
|
+
function neutralizeJsTsSafeTopLevelMergeSources(input = {}) {
|
|
6
|
+
if (typeof input.baseSourceText !== 'string'
|
|
7
|
+
|| typeof input.workerSourceText !== 'string'
|
|
8
|
+
|| typeof input.headSourceText !== 'string') {
|
|
9
|
+
return { ok: false, reasonCodes: ['missing-source-text'] };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const context = quietLedgerContext(input);
|
|
13
|
+
const base = scanJsTsTopLevelLedger(input.baseSourceText, 'base', context);
|
|
14
|
+
const worker = scanJsTsTopLevelLedger(input.workerSourceText, 'worker', context);
|
|
15
|
+
const head = scanJsTsTopLevelLedger(input.headSourceText, 'head', context);
|
|
16
|
+
if (context.conflicts.length) {
|
|
17
|
+
return { ok: false, reasonCodes: context.conflicts.map((conflict) => conflict.code) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const baseEntries = base.entries;
|
|
21
|
+
const topLevelWorker = neutralizeChangedExistingDeclarations(input.workerSourceText, worker, baseEntries);
|
|
22
|
+
const topLevelHead = neutralizeChangedExistingDeclarations(input.headSourceText, head, baseEntries);
|
|
23
|
+
const semanticWorker = neutralizeSafeTopLevelAdditions(input.workerSourceText, worker, baseEntries);
|
|
24
|
+
const semanticHead = neutralizeSafeTopLevelAdditions(input.headSourceText, head, baseEntries);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
ok: true,
|
|
28
|
+
base,
|
|
29
|
+
worker,
|
|
30
|
+
head,
|
|
31
|
+
baseSourceText: input.baseSourceText,
|
|
32
|
+
topLevelWorkerSourceText: topLevelWorker.sourceText,
|
|
33
|
+
topLevelHeadSourceText: topLevelHead.sourceText,
|
|
34
|
+
semanticWorkerSourceText: semanticWorker.sourceText,
|
|
35
|
+
semanticHeadSourceText: semanticHead.sourceText,
|
|
36
|
+
summary: {
|
|
37
|
+
workerChangedExistingDeclarations: topLevelWorker.changedExistingDeclarations,
|
|
38
|
+
headChangedExistingDeclarations: topLevelHead.changedExistingDeclarations,
|
|
39
|
+
workerNeutralizedSafeTopLevelChanges: semanticWorker.neutralizedSafeTopLevelChanges,
|
|
40
|
+
headNeutralizedSafeTopLevelChanges: semanticHead.neutralizedSafeTopLevelChanges
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createJsTsChangedDeclarationReplay(input = {}, neutralization, currentSourceText) {
|
|
46
|
+
if (!neutralization?.ok || typeof currentSourceText !== 'string') {
|
|
47
|
+
return { ok: false, reasonCodes: ['missing-neutralized-current-source'], edits: [] };
|
|
48
|
+
}
|
|
49
|
+
const context = quietLedgerContext(input);
|
|
50
|
+
const current = scanJsTsTopLevelLedger(currentSourceText, 'current', context);
|
|
51
|
+
if (context.conflicts.length) {
|
|
52
|
+
return { ok: false, reasonCodes: context.conflicts.map((conflict) => conflict.code), edits: [] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const baseEntries = neutralization.base.entries;
|
|
56
|
+
const workerMatches = matchedEntriesByBaseKey(neutralization.worker, baseEntries);
|
|
57
|
+
const headMatches = matchedEntriesByBaseKey(neutralization.head, baseEntries);
|
|
58
|
+
const currentMatches = matchedEntriesByBaseKey(current, baseEntries);
|
|
59
|
+
const edits = [];
|
|
60
|
+
const reasonCodes = [];
|
|
61
|
+
for (const baseEntry of baseEntries) {
|
|
62
|
+
if (baseEntry.kind === 'import') continue;
|
|
63
|
+
const workerEntry = workerMatches.get(baseEntry.key);
|
|
64
|
+
if (!workerEntry || sameStatementText(workerEntry.text, baseEntry.text)) continue;
|
|
65
|
+
const headEntry = headMatches.get(baseEntry.key);
|
|
66
|
+
const currentEntry = currentMatches.get(baseEntry.key);
|
|
67
|
+
if (!headEntry || !sameStatementText(headEntry.text, baseEntry.text)) {
|
|
68
|
+
reasonCodes.push(`head-anchor-changed-since-base:${baseEntry.key}`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!currentEntry || !sameStatementText(currentEntry.text, baseEntry.text)) {
|
|
72
|
+
reasonCodes.push(`current-anchor-changed-since-base:${baseEntry.key}`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
edits.push({
|
|
76
|
+
key: baseEntry.key,
|
|
77
|
+
names: workerEntry.names ?? baseEntry.names ?? [],
|
|
78
|
+
declarationKind: workerEntry.declarationInfo?.declarationKind ?? baseEntry.declarationInfo?.declarationKind,
|
|
79
|
+
start: currentEntry.start,
|
|
80
|
+
end: currentEntry.end,
|
|
81
|
+
currentText: currentEntry.text,
|
|
82
|
+
replacementText: workerEntry.text
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
ok: reasonCodes.length === 0,
|
|
87
|
+
reasonCodes,
|
|
88
|
+
edits,
|
|
89
|
+
outputSourceText: reasonCodes.length ? undefined : applySourceEdits(
|
|
90
|
+
currentSourceText,
|
|
91
|
+
edits.map((edit) => ({ start: edit.start, end: edit.end, text: edit.replacementText }))
|
|
92
|
+
)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function neutralizeChangedExistingDeclarations(sourceText, ledger, baseEntries) {
|
|
97
|
+
const usedBaseKeys = new Set();
|
|
98
|
+
const edits = [];
|
|
99
|
+
let changedExistingDeclarations = 0;
|
|
100
|
+
for (const entry of ledger.entries) {
|
|
101
|
+
const baseEntry = findBaseEntry(entry, baseEntries, usedBaseKeys);
|
|
102
|
+
if (!baseEntry) continue;
|
|
103
|
+
usedBaseKeys.add(baseEntry.key);
|
|
104
|
+
if (entry.kind === 'import' || sameStatementText(entry.text, baseEntry.text)) continue;
|
|
105
|
+
edits.push({ start: entry.start, end: entry.end, text: baseEntry.text });
|
|
106
|
+
changedExistingDeclarations += 1;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
sourceText: applySourceEdits(sourceText, edits),
|
|
110
|
+
changedExistingDeclarations
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function neutralizeSafeTopLevelAdditions(sourceText, ledger, baseEntries) {
|
|
115
|
+
const usedBaseKeys = new Set();
|
|
116
|
+
const edits = [];
|
|
117
|
+
let neutralizedSafeTopLevelChanges = 0;
|
|
118
|
+
for (const entry of ledger.entries) {
|
|
119
|
+
const baseEntry = findBaseEntry(entry, baseEntries, usedBaseKeys);
|
|
120
|
+
if (!baseEntry) {
|
|
121
|
+
edits.push(deleteEntryEdit(sourceText, entry));
|
|
122
|
+
neutralizedSafeTopLevelChanges += 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
usedBaseKeys.add(baseEntry.key);
|
|
126
|
+
if (entry.kind !== 'import' || sameStatementText(entry.text, baseEntry.text)) continue;
|
|
127
|
+
edits.push({ start: entry.start, end: entry.end, text: baseEntry.text });
|
|
128
|
+
neutralizedSafeTopLevelChanges += 1;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
sourceText: applySourceEdits(sourceText, edits),
|
|
132
|
+
neutralizedSafeTopLevelChanges
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function findBaseEntry(entry, baseEntries, usedBaseKeys) {
|
|
137
|
+
if (!entry) return undefined;
|
|
138
|
+
const direct = baseEntries.find((candidate) => candidate.key === entry.key && !usedBaseKeys.has(candidate.key));
|
|
139
|
+
if (direct) return direct;
|
|
140
|
+
return entry.kind === 'import'
|
|
141
|
+
? findCompatibleBaseImportEntry(entry, baseEntries, usedBaseKeys)
|
|
142
|
+
?? findSameImportTargetBaseEntry(entry, baseEntries, usedBaseKeys)
|
|
143
|
+
: undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function matchedEntriesByBaseKey(ledger, baseEntries) {
|
|
147
|
+
const usedBaseKeys = new Set();
|
|
148
|
+
const matches = new Map();
|
|
149
|
+
for (const entry of ledger.entries) {
|
|
150
|
+
const baseEntry = findBaseEntry(entry, baseEntries, usedBaseKeys);
|
|
151
|
+
if (!baseEntry) continue;
|
|
152
|
+
usedBaseKeys.add(baseEntry.key);
|
|
153
|
+
matches.set(baseEntry.key, entry);
|
|
154
|
+
}
|
|
155
|
+
return matches;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function deleteEntryEdit(sourceText, entry) {
|
|
159
|
+
const lineEnd = sourceText.indexOf('\n', entry.end);
|
|
160
|
+
const end = lineEnd === -1 ? entry.end : lineEnd + 1;
|
|
161
|
+
const lineStart = sourceText.lastIndexOf('\n', Math.max(0, entry.start - 1)) + 1;
|
|
162
|
+
const start = onlyWhitespace(sourceText.slice(lineStart, entry.start)) ? lineStart : entry.start;
|
|
163
|
+
return { start, end, text: '' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function onlyWhitespace(text) {
|
|
167
|
+
return /^[\t ]*$/.test(text);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function applySourceEdits(sourceText, edits) {
|
|
171
|
+
return [...edits]
|
|
172
|
+
.sort((left, right) => right.start - left.start || right.end - left.end)
|
|
173
|
+
.reduce((current, edit) => `${current.slice(0, edit.start)}${edit.text}${current.slice(edit.end)}`, sourceText);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function quietLedgerContext(input) {
|
|
177
|
+
return {
|
|
178
|
+
id: String(input.id ?? 'js_ts_safe_merge'),
|
|
179
|
+
sourcePath: input.sourcePath,
|
|
180
|
+
language: input.language ?? 'typescript',
|
|
181
|
+
conflicts: [],
|
|
182
|
+
blockedGateIds: new Set(),
|
|
183
|
+
gateReasonCodes: new Map()
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export {
|
|
188
|
+
createJsTsChangedDeclarationReplay,
|
|
189
|
+
neutralizeJsTsSafeTopLevelMergeSources
|
|
190
|
+
};
|
|
@@ -12,6 +12,20 @@ function canonicalizeSourceBodies(sourceText, preparedRegions, side) {
|
|
|
12
12
|
return output;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function removePreparedMemberAdditions(sourceText, preparedRegions, side) {
|
|
16
|
+
let output = sourceText;
|
|
17
|
+
const replacements = preparedRegions
|
|
18
|
+
.map((region) => ({
|
|
19
|
+
range: region[side],
|
|
20
|
+
replacement: removeMembersFromBody(region[side].body, region[`${side}AddedMembers`] ?? [], region.kind)
|
|
21
|
+
}))
|
|
22
|
+
.sort((left, right) => right.range.bodyStart - left.range.bodyStart);
|
|
23
|
+
for (const { range, replacement } of replacements) {
|
|
24
|
+
output = `${output.slice(0, range.bodyStart)}${replacement}${output.slice(range.bodyEnd)}`;
|
|
25
|
+
}
|
|
26
|
+
return output;
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
function applyMemberAdditions(headSourceText, preparedRegions) {
|
|
16
30
|
return applyPreparedMemberAdditions(headSourceText, preparedRegions, ['worker']).sourceText;
|
|
17
31
|
}
|
|
@@ -28,7 +42,7 @@ function applyPreparedMemberAdditions(sourceText, preparedRegions, sides = ['wor
|
|
|
28
42
|
}
|
|
29
43
|
const members = sides.flatMap((side) => region[`${side}AddedMembers`] ?? []);
|
|
30
44
|
if (!members.length) return undefined;
|
|
31
|
-
return { range: match.value, replacement: appendMembersToBody(match.value.body, members) };
|
|
45
|
+
return { range: match.value, replacement: appendMembersToBody(match.value.body, members, region.kind) };
|
|
32
46
|
})
|
|
33
47
|
.filter(Boolean)
|
|
34
48
|
.sort((left, right) => right.range.bodyStart - left.range.bodyStart);
|
|
@@ -38,32 +52,50 @@ function applyPreparedMemberAdditions(sourceText, preparedRegions, sides = ['wor
|
|
|
38
52
|
return { sourceText: output, reasonCodes: uniqueStrings(reasonCodes) };
|
|
39
53
|
}
|
|
40
54
|
|
|
41
|
-
function appendMembersToBody(body, members) {
|
|
55
|
+
function appendMembersToBody(body, members, kind) {
|
|
42
56
|
if (!members.length) return body;
|
|
43
57
|
const indent = inferMemberIndent(body) ?? inferMemberIndent(members.map((member) => member.text).join('\n')) ?? ' ';
|
|
44
|
-
const addedText = members.map((member) => normalizeMemberForInsertion(member.text, indent)).join('\n');
|
|
58
|
+
const addedText = members.map((member, index) => normalizeMemberForInsertion(member.text, indent, kind, index < members.length - 1)).join('\n');
|
|
45
59
|
const trailing = body.match(/\s*$/)?.[0] ?? '';
|
|
46
60
|
const before = body.slice(0, body.length - trailing.length);
|
|
47
61
|
if (!before.trim()) {
|
|
48
62
|
const closingIndent = trailing.includes('\n') ? trailing.slice(trailing.lastIndexOf('\n') + 1) : '';
|
|
49
63
|
return `\n${addedText}\n${closingIndent}`;
|
|
50
64
|
}
|
|
51
|
-
|
|
65
|
+
const appendableBefore = appendReadyBody(before, kind);
|
|
66
|
+
return `${appendableBefore}${appendableBefore.endsWith('\n') ? '' : '\n'}${addedText}${trailing.includes('\n') ? trailing : `\n${trailing}`}`;
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
function inferMemberIndent(text) {
|
|
55
70
|
return String(text ?? '').match(/\n([ \t]*)\S/)?.[1];
|
|
56
71
|
}
|
|
57
72
|
|
|
58
|
-
function normalizeMemberForInsertion(text, indent) {
|
|
73
|
+
function normalizeMemberForInsertion(text, indent, kind, needsDelimiter) {
|
|
59
74
|
const lines = String(text ?? '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
|
60
75
|
while (lines.length && !lines[0].trim()) lines.shift();
|
|
61
76
|
while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
|
|
62
77
|
const commonIndent = minimumIndent(lines);
|
|
63
|
-
|
|
78
|
+
const normalized = lines.map((line) => {
|
|
64
79
|
const normalized = commonIndent ? line.slice(Math.min(commonIndent, leadingWhitespace(line))) : line;
|
|
65
80
|
return normalized.trim() ? `${indent}${normalized}` : normalized;
|
|
66
81
|
}).join('\n');
|
|
82
|
+
if (kind !== 'object' || !needsDelimiter || /,\s*$/.test(normalized)) return normalized;
|
|
83
|
+
return `${normalized},`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function appendReadyBody(before, kind) {
|
|
87
|
+
if (kind !== 'object' || !before.trim() || /,\s*$/.test(before)) return before;
|
|
88
|
+
return `${before.replace(/\s*$/, '')},`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function removeMembersFromBody(body, members, kind) {
|
|
92
|
+
let output = body;
|
|
93
|
+
for (const member of [...members].sort((left, right) => right.start - left.start || right.end - left.end)) {
|
|
94
|
+
output = `${output.slice(0, member.start)}${output.slice(member.end)}`;
|
|
95
|
+
}
|
|
96
|
+
if (kind !== 'object') return output;
|
|
97
|
+
const trailing = String(body ?? '').match(/\s*$/)?.[0] ?? '';
|
|
98
|
+
return output.replace(/,\s*$/, trailing);
|
|
67
99
|
}
|
|
68
100
|
|
|
69
101
|
function minimumIndent(lines) {
|
|
@@ -78,5 +110,6 @@ function leadingWhitespace(line) {
|
|
|
78
110
|
export {
|
|
79
111
|
applyMemberAdditions,
|
|
80
112
|
applyPreparedMemberAdditions,
|
|
81
|
-
canonicalizeSourceBodies
|
|
113
|
+
canonicalizeSourceBodies,
|
|
114
|
+
removePreparedMemberAdditions
|
|
82
115
|
};
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
function normalizeMemberText(text) {
|
|
2
|
-
|
|
1
|
+
function normalizeMemberText(text, kind) {
|
|
2
|
+
const normalized = String(text ?? '')
|
|
3
3
|
.replace(/\r\n/g, '\n')
|
|
4
4
|
.replace(/\r/g, '\n')
|
|
5
5
|
.split('\n')
|
|
6
6
|
.map((line) => line.trimEnd())
|
|
7
7
|
.join('\n')
|
|
8
8
|
.trim();
|
|
9
|
+
if (kind === 'object') return normalized.replace(/,\s*$/, '');
|
|
10
|
+
if (kind === 'interface' || kind === 'type' || kind === 'class') return normalized.replace(/;\s*$/, '');
|
|
11
|
+
return normalized;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
function uniqueStrings(values) {
|
package/package.json
CHANGED