@shapeshift-labs/frontier-lang-compiler 0.2.149 → 0.2.150
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/declarations/semantic-patch-bundle-composition.d.ts +62 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/internal/index-impl/semanticPatchBundleComposition.js +166 -0
- package/dist/js-ts-safe-merge-context.js +1 -0
- package/dist/js-ts-safe-merge-jsx-attribute-fallback.js +320 -0
- package/dist/js-ts-safe-merge-plan.js +3 -1
- package/dist/js-ts-safe-merge-semantic-edit-fallback.js +41 -0
- package/dist/js-ts-safe-merge-source-shape-fallbacks.js +3 -1
- package/dist/js-ts-safe-project-merge.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { EvidenceRecord, FrontierSourceLanguage } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import type { SemanticEditProjection, SemanticEditReplay } from './semantic-edit-script.js';
|
|
3
|
+
import type { SemanticPatchBundleRecord } from './semantic-patch-bundle.js';
|
|
4
|
+
import type { CompareSemanticPatchBundleRecordsOptions, SemanticPatchBundleOverlapRecord } from './semantic-patch-bundle-overlaps.js';
|
|
5
|
+
|
|
6
|
+
export type SemanticPatchBundleCompositionStatus = 'verified' | 'blocked' | string;
|
|
7
|
+
|
|
8
|
+
export interface ComposeSemanticPatchBundleProjectionsInput {
|
|
9
|
+
readonly id?: string;
|
|
10
|
+
readonly sourcePath?: string;
|
|
11
|
+
readonly language?: FrontierSourceLanguage | string;
|
|
12
|
+
readonly currentSourceText?: string;
|
|
13
|
+
readonly headSourceText?: string;
|
|
14
|
+
readonly bundles?: readonly SemanticPatchBundleRecord[] | SemanticPatchBundleRecord;
|
|
15
|
+
readonly semanticPatchBundles?: readonly SemanticPatchBundleRecord[] | SemanticPatchBundleRecord;
|
|
16
|
+
readonly projections?: readonly SemanticEditProjection[] | SemanticEditProjection;
|
|
17
|
+
readonly semanticEditProjections?: readonly SemanticEditProjection[] | SemanticEditProjection;
|
|
18
|
+
readonly overlapOptions?: CompareSemanticPatchBundleRecordsOptions;
|
|
19
|
+
readonly parser?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SemanticPatchBundleComposition {
|
|
23
|
+
readonly kind: 'frontier.lang.semanticPatchBundleComposition';
|
|
24
|
+
readonly version: 1;
|
|
25
|
+
readonly schema: 'frontier.lang.semanticPatchBundleComposition.v1';
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly hash: string;
|
|
28
|
+
readonly status: SemanticPatchBundleCompositionStatus;
|
|
29
|
+
readonly sourcePath?: string;
|
|
30
|
+
readonly language?: FrontierSourceLanguage | string;
|
|
31
|
+
readonly bundleIds: readonly string[];
|
|
32
|
+
readonly projectionIds: readonly string[];
|
|
33
|
+
readonly currentHash?: string;
|
|
34
|
+
readonly outputHash?: string;
|
|
35
|
+
readonly outputSourceText?: string;
|
|
36
|
+
readonly replays: readonly SemanticEditReplay[];
|
|
37
|
+
readonly verificationReplays: readonly SemanticEditReplay[];
|
|
38
|
+
readonly overlapRecords: readonly SemanticPatchBundleOverlapRecord[];
|
|
39
|
+
readonly admission: {
|
|
40
|
+
readonly status: 'auto-merge-candidate' | 'blocked' | string;
|
|
41
|
+
readonly action: 'apply' | 'human-review' | string;
|
|
42
|
+
readonly reviewRequired: boolean;
|
|
43
|
+
readonly autoApplyCandidate: boolean;
|
|
44
|
+
readonly autoMergeClaim: false;
|
|
45
|
+
readonly semanticEquivalenceClaim: false;
|
|
46
|
+
readonly reasonCodes: readonly string[];
|
|
47
|
+
};
|
|
48
|
+
readonly summary: {
|
|
49
|
+
readonly bundles: number;
|
|
50
|
+
readonly projections: number;
|
|
51
|
+
readonly replays: number;
|
|
52
|
+
readonly verificationReplays: number;
|
|
53
|
+
readonly appliedEdits: number;
|
|
54
|
+
readonly overlapRecords: number;
|
|
55
|
+
readonly blockedOverlaps: number;
|
|
56
|
+
};
|
|
57
|
+
readonly evidence: readonly EvidenceRecord[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export declare function composeSemanticPatchBundleProjections(
|
|
61
|
+
input?: ComposeSemanticPatchBundleProjectionsInput
|
|
62
|
+
): SemanticPatchBundleComposition;
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export * from './declarations/semantic-edit-bundle.js';
|
|
|
24
24
|
export * from './declarations/semantic-patch-bundle-index.js';
|
|
25
25
|
export * from './declarations/semantic-patch-bundle.js';
|
|
26
26
|
export * from './declarations/semantic-patch-bundle-overlaps.js';
|
|
27
|
+
export * from './declarations/semantic-patch-bundle-composition.js';
|
|
27
28
|
export * from './declarations/semantic-transform-identity.js';
|
|
28
29
|
export * from './declarations/bidirectional-target-change.js';
|
|
29
30
|
export * from './declarations/bidirectional-target-change-source-edit.js';
|
package/dist/index.js
CHANGED
|
@@ -72,6 +72,7 @@ export { queryProjectionReadinessMatrix } from './internal/index-impl/queryProje
|
|
|
72
72
|
export { createSemanticPatchBundleRecord, querySemanticPatchBundleRecords, SemanticPatchBundleAdmissionStatuses } from './internal/index-impl/semanticPatchBundleRecords.js';
|
|
73
73
|
export { createSemanticEditBundleAdmission, SemanticEditBundleAdmissionStatuses } from './internal/index-impl/semanticEditBundleAdmission.js';
|
|
74
74
|
export { compareSemanticPatchBundleRecords, querySemanticPatchBundleOverlaps, SemanticPatchBundleOverlapKinds, SemanticPatchBundleOverlapStatuses } from './internal/index-impl/semanticPatchBundleOverlaps.js';
|
|
75
|
+
export { composeSemanticPatchBundleProjections } from './internal/index-impl/semanticPatchBundleComposition.js';
|
|
75
76
|
export { createSemanticTransformIdentityRecord, deriveSemanticTransformIdentityRecords, semanticTransformIdentityFields } from './internal/index-impl/semanticTransformIdentityRecords.js';
|
|
76
77
|
export { createSemanticMergeCandidateAdmissionRecord, decorateSemanticMergeCandidateForAdmission, querySemanticMergeCandidateAdmissionOverlaps, SemanticMergeCandidateProjectionRisks, semanticMergeCandidateReadinessSortKey, sortSemanticMergeCandidateAdmissionRecords } from './internal/index-impl/semanticMergeCandidateRecords.js';
|
|
77
78
|
export { querySemanticMergeConflictClasses, SemanticMergeConflictClasses, semanticMergeConflictRiskScore, sortSemanticMergeCandidatesByConflictRisk, summarizeSemanticMergeConflicts } from './internal/index-impl/semanticMergeConflicts.js';
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import { idFragment, uniqueStrings } from '../../native-import-utils.js';
|
|
3
|
+
import { compareSemanticPatchBundleRecords } from './semanticPatchBundleOverlaps.js';
|
|
4
|
+
import { replaySemanticEditProjection } from './replaySemanticEditProjection.js';
|
|
5
|
+
|
|
6
|
+
function composeSemanticPatchBundleProjections(input = {}) {
|
|
7
|
+
const id = String(input.id ?? 'semantic_patch_bundle_composition');
|
|
8
|
+
const currentSourceText = input.currentSourceText ?? input.headSourceText;
|
|
9
|
+
const projections = array(input.projections ?? input.semanticEditProjections);
|
|
10
|
+
const bundles = array(input.bundles ?? input.semanticPatchBundles);
|
|
11
|
+
const sourcePath = input.sourcePath ?? projections.find((projection) => projection?.sourcePath)?.sourcePath;
|
|
12
|
+
const language = input.language ?? projections.find((projection) => projection?.language)?.language;
|
|
13
|
+
const reasonCodes = baseReasonCodes({ currentSourceText, projections, bundles });
|
|
14
|
+
const overlapRecords = bundleOverlapRecords(bundles, input.overlapOptions);
|
|
15
|
+
reasonCodes.push(...overlapRecords.flatMap((record) => record.admission.status === 'independent'
|
|
16
|
+
? []
|
|
17
|
+
: [`bundle-overlap:${record.admission.status}`]));
|
|
18
|
+
const replays = reasonCodes.length ? [] : projections.map((projection, index) => replaySemanticEditProjection({
|
|
19
|
+
id: `${id}_replay_${index + 1}`,
|
|
20
|
+
projection,
|
|
21
|
+
currentSourceText,
|
|
22
|
+
currentSourcePath: sourcePath ?? projection.sourcePath,
|
|
23
|
+
language: language ?? projection.language,
|
|
24
|
+
parser: input.parser
|
|
25
|
+
}));
|
|
26
|
+
reasonCodes.push(...replayReasonCodes(replays));
|
|
27
|
+
const edits = reasonCodes.length ? [] : appliedReplayEdits(replays);
|
|
28
|
+
reasonCodes.push(...sourceEditOverlapReasons(edits));
|
|
29
|
+
const outputSourceText = reasonCodes.length ? undefined : applyReplayEdits(currentSourceText, edits);
|
|
30
|
+
const verificationReplays = outputSourceText === undefined ? [] : projections.map((projection, index) => replaySemanticEditProjection({
|
|
31
|
+
id: `${id}_already_applied_${index + 1}`,
|
|
32
|
+
projection,
|
|
33
|
+
currentSourceText: outputSourceText,
|
|
34
|
+
currentSourcePath: sourcePath ?? projection.sourcePath,
|
|
35
|
+
language: language ?? projection.language,
|
|
36
|
+
parser: input.parser
|
|
37
|
+
}));
|
|
38
|
+
reasonCodes.push(...verificationReasonCodes(verificationReplays));
|
|
39
|
+
const status = reasonCodes.length ? 'blocked' : 'verified';
|
|
40
|
+
const core = {
|
|
41
|
+
kind: 'frontier.lang.semanticPatchBundleComposition',
|
|
42
|
+
version: 1,
|
|
43
|
+
schema: 'frontier.lang.semanticPatchBundleComposition.v1',
|
|
44
|
+
id,
|
|
45
|
+
status,
|
|
46
|
+
sourcePath,
|
|
47
|
+
language,
|
|
48
|
+
bundleIds: bundles.map((bundle) => bundle?.id).filter(Boolean),
|
|
49
|
+
projectionIds: projections.map((projection) => projection?.id).filter(Boolean),
|
|
50
|
+
currentHash: typeof currentSourceText === 'string' ? hashSemanticValue(currentSourceText) : undefined,
|
|
51
|
+
outputHash: outputSourceText === undefined ? undefined : hashSemanticValue(outputSourceText),
|
|
52
|
+
outputSourceText,
|
|
53
|
+
replays,
|
|
54
|
+
verificationReplays,
|
|
55
|
+
overlapRecords,
|
|
56
|
+
admission: {
|
|
57
|
+
status: status === 'verified' ? 'auto-merge-candidate' : 'blocked',
|
|
58
|
+
action: status === 'verified' ? 'apply' : 'human-review',
|
|
59
|
+
reviewRequired: status !== 'verified',
|
|
60
|
+
autoApplyCandidate: status === 'verified',
|
|
61
|
+
autoMergeClaim: false,
|
|
62
|
+
semanticEquivalenceClaim: false,
|
|
63
|
+
reasonCodes: uniqueStrings(reasonCodes)
|
|
64
|
+
},
|
|
65
|
+
summary: {
|
|
66
|
+
bundles: bundles.length,
|
|
67
|
+
projections: projections.length,
|
|
68
|
+
replays: replays.length,
|
|
69
|
+
verificationReplays: verificationReplays.length,
|
|
70
|
+
appliedEdits: edits.length,
|
|
71
|
+
overlapRecords: overlapRecords.length,
|
|
72
|
+
blockedOverlaps: overlapRecords.filter((record) => record.admission.status !== 'independent').length
|
|
73
|
+
},
|
|
74
|
+
evidence: [{
|
|
75
|
+
id: `evidence_${idFragment(id)}_semantic_patch_bundle_composition`,
|
|
76
|
+
kind: 'semantic-patch-bundle-composition',
|
|
77
|
+
status: status === 'verified' ? 'passed' : 'needs-review',
|
|
78
|
+
path: sourcePath,
|
|
79
|
+
summary: status === 'verified'
|
|
80
|
+
? `Composed ${edits.length} replayed semantic edit(s) from ${projections.length} projection(s).`
|
|
81
|
+
: `Semantic patch bundle composition blocked: ${uniqueStrings(reasonCodes).join(', ')}.`,
|
|
82
|
+
metadata: {
|
|
83
|
+
bundleIds: bundles.map((bundle) => bundle?.id).filter(Boolean),
|
|
84
|
+
projectionIds: projections.map((projection) => projection?.id).filter(Boolean),
|
|
85
|
+
autoMergeClaim: false,
|
|
86
|
+
semanticEquivalenceClaim: false
|
|
87
|
+
}
|
|
88
|
+
}]
|
|
89
|
+
};
|
|
90
|
+
return { ...core, hash: hashSemanticValue(core) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function baseReasonCodes(input) {
|
|
94
|
+
return uniqueStrings([
|
|
95
|
+
typeof input.currentSourceText === 'string' ? undefined : 'missing-current-source-text',
|
|
96
|
+
input.projections.length ? undefined : 'missing-semantic-edit-projections',
|
|
97
|
+
input.bundles.length && input.bundles.length !== input.projections.length ? 'bundle-projection-count-mismatch' : undefined,
|
|
98
|
+
...input.projections.map((projection, index) => projection?.status === 'projected' ? undefined : `projection-not-projected:${index + 1}`)
|
|
99
|
+
].filter(Boolean));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function bundleOverlapRecords(bundles, options = {}) {
|
|
103
|
+
const records = [];
|
|
104
|
+
for (let left = 0; left < bundles.length; left += 1) {
|
|
105
|
+
for (let right = left + 1; right < bundles.length; right += 1) {
|
|
106
|
+
records.push(compareSemanticPatchBundleRecords(bundles[left], bundles[right], options));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return records;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function replayReasonCodes(replays) {
|
|
113
|
+
return uniqueStrings(replays.flatMap((replay, index) => {
|
|
114
|
+
if (replay.status === 'accepted-clean' || replay.status === 'already-applied') return [];
|
|
115
|
+
return [`replay-not-clean:${index + 1}:${replay.status}`, ...(replay.admission?.reasonCodes ?? replay.summary?.reasonCodes ?? [])];
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function appliedReplayEdits(replays) {
|
|
120
|
+
return replays.flatMap((replay, replayIndex) => (replay.edits ?? [])
|
|
121
|
+
.filter((edit) => edit.status === 'applied')
|
|
122
|
+
.map((edit, editIndex) => ({
|
|
123
|
+
replayIndex,
|
|
124
|
+
editIndex,
|
|
125
|
+
operationId: edit.operationId,
|
|
126
|
+
start: edit.start,
|
|
127
|
+
end: edit.end,
|
|
128
|
+
replacementText: edit.replacementText
|
|
129
|
+
})));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sourceEditOverlapReasons(edits) {
|
|
133
|
+
const reasons = [];
|
|
134
|
+
const ordered = edits.slice().sort((left, right) => left.start - right.start || left.end - right.end);
|
|
135
|
+
for (let index = 1; index < ordered.length; index += 1) {
|
|
136
|
+
const previous = ordered[index - 1];
|
|
137
|
+
const current = ordered[index];
|
|
138
|
+
if (sourceEditsOverlap(previous, current)) reasons.push(`composed-source-edit-overlap:${previous.operationId}:${current.operationId}`);
|
|
139
|
+
}
|
|
140
|
+
return uniqueStrings(reasons);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function applyReplayEdits(sourceText, edits) {
|
|
144
|
+
return edits.slice().sort((left, right) => right.start - left.start || right.end - left.end)
|
|
145
|
+
.reduce((text, edit) => text.slice(0, edit.start) + edit.replacementText + text.slice(edit.end), sourceText);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function verificationReasonCodes(replays) {
|
|
149
|
+
return uniqueStrings(replays.flatMap((replay, index) => {
|
|
150
|
+
if (replay.status === 'already-applied') return [];
|
|
151
|
+
return [`verification-replay-not-already-applied:${index + 1}:${replay.status}`, ...(replay.admission?.reasonCodes ?? [])];
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function sourceEditsOverlap(left, right) {
|
|
156
|
+
if (left.start === left.end) return right.start < left.start && left.start < right.end;
|
|
157
|
+
if (right.start === right.end) return left.start < right.start && right.start < left.end;
|
|
158
|
+
return left.start < right.end && right.start < left.end;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function array(value) {
|
|
162
|
+
if (value === undefined || value === null) return [];
|
|
163
|
+
return Array.isArray(value) ? value : [value];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export { composeSemanticPatchBundleProjections };
|
|
@@ -6,6 +6,7 @@ export function createMergeContext(input) {
|
|
|
6
6
|
sourcePath: input.sourcePath,
|
|
7
7
|
language: input.language ?? 'typescript',
|
|
8
8
|
deferReExportIdentityConflictsToProjectGraph: input.deferReExportIdentityConflictsToProjectGraph === true,
|
|
9
|
+
deferTopLevelRenamePublicExportContractToProjectGraph: input.deferTopLevelRenamePublicExportContractToProjectGraph === true,
|
|
9
10
|
conflicts: [],
|
|
10
11
|
blockedGateIds: new Set(),
|
|
11
12
|
gateReasonCodes: new Map()
|
|
@@ -0,0 +1,320 @@
|
|
|
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
|
+
|
|
6
|
+
function createJsxAttributeSemanticFallbackResult(input, topLevelResult, stagedFallback) {
|
|
7
|
+
const currentSourceText = stagedFallback?.directReplayCurrentSourceText
|
|
8
|
+
?? stagedFallback?.replayCurrentSourceText
|
|
9
|
+
?? input.headSourceText;
|
|
10
|
+
const merge = mergeJsxAttributeSources({
|
|
11
|
+
baseSourceText: input.baseSourceText,
|
|
12
|
+
workerSourceText: input.workerSourceText,
|
|
13
|
+
headSourceText: input.headSourceText,
|
|
14
|
+
currentSourceText
|
|
15
|
+
});
|
|
16
|
+
if (!merge.ok || merge.sourceText === currentSourceText) return undefined;
|
|
17
|
+
const resultBase = stagedFallback?.stagedTopLevelResult ?? topLevelResult;
|
|
18
|
+
const language = input.language ?? topLevelResult.language ?? 'tsx';
|
|
19
|
+
const sourcePath = input.sourcePath ?? topLevelResult.sourcePath ?? 'inline.tsx';
|
|
20
|
+
const id = String(input.id ?? topLevelResult.id ?? 'js_ts_safe_merge');
|
|
21
|
+
const artifacts = createJsTsSafeMergeSemanticArtifacts({
|
|
22
|
+
...input,
|
|
23
|
+
id: `${id}_jsx_attribute`,
|
|
24
|
+
language,
|
|
25
|
+
sourcePath,
|
|
26
|
+
headSourceText: currentSourceText,
|
|
27
|
+
headHash: undefined,
|
|
28
|
+
currentSourceHash: undefined
|
|
29
|
+
}, {
|
|
30
|
+
...resultBase,
|
|
31
|
+
id: `${String(input.id ?? resultBase.id ?? 'js_ts_safe_merge')}_jsx_attribute`,
|
|
32
|
+
language,
|
|
33
|
+
sourcePath,
|
|
34
|
+
mergedSourceText: merge.sourceText,
|
|
35
|
+
outputSourceText: merge.sourceText
|
|
36
|
+
});
|
|
37
|
+
if (artifacts.status !== 'verified') return undefined;
|
|
38
|
+
const gates = semanticArtifactGates(artifacts);
|
|
39
|
+
return {
|
|
40
|
+
...resultBase,
|
|
41
|
+
id: String(input.id ?? resultBase.id ?? topLevelResult.id),
|
|
42
|
+
status: JsTsSafeMergeStatuses.merged,
|
|
43
|
+
mergedSourceText: merge.sourceText,
|
|
44
|
+
outputSourceText: merge.sourceText,
|
|
45
|
+
conflicts: [],
|
|
46
|
+
gates,
|
|
47
|
+
admission: {
|
|
48
|
+
status: 'auto-merge-candidate',
|
|
49
|
+
action: 'apply',
|
|
50
|
+
reviewRequired: false,
|
|
51
|
+
autoApplyCandidate: true,
|
|
52
|
+
autoMergeClaim: false,
|
|
53
|
+
semanticEquivalenceClaim: false,
|
|
54
|
+
reasonCodes: []
|
|
55
|
+
},
|
|
56
|
+
summary: {
|
|
57
|
+
...resultBase.summary,
|
|
58
|
+
changedExistingDeclarations: semanticFallbackChangedExistingDeclarations(topLevelResult, resultBase, stagedFallback),
|
|
59
|
+
conflicts: 0,
|
|
60
|
+
gatesPassed: gates.filter((gate) => gate.status === 'passed').length,
|
|
61
|
+
semanticEditOperations: artifacts.script.summary.operations,
|
|
62
|
+
semanticEditAppliedOperations: artifacts.replay.summary.applied,
|
|
63
|
+
semanticEditReplayStatus: artifacts.replay.status,
|
|
64
|
+
jsxAttributeTags: merge.summary.tags,
|
|
65
|
+
jsxAttributeEdits: merge.summary.edits,
|
|
66
|
+
composedPhases: 2
|
|
67
|
+
},
|
|
68
|
+
metadata: {
|
|
69
|
+
...resultBase.metadata,
|
|
70
|
+
composed: {
|
|
71
|
+
phase: stagedFallback
|
|
72
|
+
? 'staged-top-level-jsx-attribute-semantic-fallback'
|
|
73
|
+
: 'jsx-attribute-semantic-fallback',
|
|
74
|
+
phases: stagedFallback
|
|
75
|
+
? ['top-level-neutralization', 'top-level-ledger', 'jsx-attribute']
|
|
76
|
+
: ['top-level-ledger', 'jsx-attribute'],
|
|
77
|
+
originalReasonCodes: topLevelResult.admission?.reasonCodes ?? [],
|
|
78
|
+
stagedTopLevelSummary: stagedFallback?.stagedTopLevelResult?.summary,
|
|
79
|
+
neutralization: stagedFallback?.neutralization?.summary,
|
|
80
|
+
jsxAttributeFallback: merge.summary
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
semanticArtifacts: artifacts
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mergeJsxAttributeSources(input) {
|
|
88
|
+
if (![input.baseSourceText, input.workerSourceText, input.headSourceText, input.currentSourceText].every(isString)) {
|
|
89
|
+
return blocked('missing-source-text');
|
|
90
|
+
}
|
|
91
|
+
const parsed = ['base', 'worker', 'head', 'current'].map((side) => parseJsxTags(input[`${side}SourceText`]));
|
|
92
|
+
if (parsed.some((source) => source.reasonCodes.length)) return blocked('jsx-attribute-parse-blocked');
|
|
93
|
+
const [base, worker, head, current] = parsed;
|
|
94
|
+
const edits = [];
|
|
95
|
+
let changedTags = 0;
|
|
96
|
+
for (const baseTag of base.tags) {
|
|
97
|
+
const workerTag = worker.byKey.get(baseTag.key);
|
|
98
|
+
const headTag = head.byKey.get(baseTag.key);
|
|
99
|
+
const currentTag = current.byKey.get(baseTag.key);
|
|
100
|
+
if (!workerTag || !headTag || !currentTag) continue;
|
|
101
|
+
if (sameTagText(baseTag, workerTag)) continue;
|
|
102
|
+
const merged = mergeTagAttributes(baseTag, workerTag, headTag, currentTag);
|
|
103
|
+
if (merged.status === 'blocked') return blocked(...merged.reasonCodes);
|
|
104
|
+
for (const edit of merged.edits) edits.push(edit);
|
|
105
|
+
if (merged.edits.length) changedTags += 1;
|
|
106
|
+
}
|
|
107
|
+
if (!edits.length) return blocked('no-jsx-attribute-merge-candidate');
|
|
108
|
+
const sourceText = edits.sort((left, right) => right.start - left.start)
|
|
109
|
+
.reduce((text, edit) => text.slice(0, edit.start) + edit.replacement + text.slice(edit.end), input.currentSourceText);
|
|
110
|
+
return { ok: true, sourceText, summary: { tags: changedTags, edits: edits.length } };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function mergeTagAttributes(base, worker, head, current) {
|
|
114
|
+
if (![worker, head, current].every((tag) => tag.tagName === base.tagName)) return blockedTag('jsx-tag-name-changed');
|
|
115
|
+
const maps = [base, worker, head, current].map(attributeMap);
|
|
116
|
+
if (maps.some((map) => map.reasonCodes.length)) return blockedTag('jsx-attribute-duplicate-name');
|
|
117
|
+
if (![worker, head, current].every((tag) => sameAttributeNames(base, tag))) {
|
|
118
|
+
return blockedTag('jsx-attribute-shape-changed');
|
|
119
|
+
}
|
|
120
|
+
const [, workerAttrs, headAttrs, currentAttrs] = maps.map((map) => map.byName);
|
|
121
|
+
const edits = [];
|
|
122
|
+
for (const baseAttr of base.attributes) {
|
|
123
|
+
const workerAttr = workerAttrs.get(baseAttr.name);
|
|
124
|
+
const headAttr = headAttrs.get(baseAttr.name);
|
|
125
|
+
const currentAttr = currentAttrs.get(baseAttr.name);
|
|
126
|
+
const workerChanged = !sameAttrText(baseAttr, workerAttr);
|
|
127
|
+
const headChanged = !sameAttrText(baseAttr, headAttr);
|
|
128
|
+
if (workerChanged && headChanged && !sameAttrText(workerAttr, headAttr)) {
|
|
129
|
+
return blockedTag('jsx-attribute-conflict');
|
|
130
|
+
}
|
|
131
|
+
if (!sameAttrText(currentAttr, headAttr) && !sameAttrText(currentAttr, workerAttr)) {
|
|
132
|
+
return blockedTag('jsx-attribute-current-diverged');
|
|
133
|
+
}
|
|
134
|
+
if (workerChanged && !sameAttrText(currentAttr, workerAttr)) {
|
|
135
|
+
edits.push({ start: currentAttr.start, end: currentAttr.end, replacement: workerAttr.text });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { status: 'merged', edits };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseJsxTags(sourceText) {
|
|
142
|
+
const tags = [];
|
|
143
|
+
const reasonCodes = [];
|
|
144
|
+
const ordinals = new Map();
|
|
145
|
+
let index = 0;
|
|
146
|
+
while (index < sourceText.length) {
|
|
147
|
+
const start = sourceText.indexOf('<', index);
|
|
148
|
+
if (start === -1) break;
|
|
149
|
+
const parsed = parseOpeningTag(sourceText, start);
|
|
150
|
+
if (!parsed) {
|
|
151
|
+
index = start + 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (parsed.reasonCodes.length) reasonCodes.push(...parsed.reasonCodes);
|
|
155
|
+
const ordinal = (ordinals.get(parsed.tagName) ?? 0) + 1;
|
|
156
|
+
ordinals.set(parsed.tagName, ordinal);
|
|
157
|
+
tags.push({ ...parsed, key: `${parsed.tagName}#${ordinal}` });
|
|
158
|
+
index = parsed.end;
|
|
159
|
+
}
|
|
160
|
+
return { tags, byKey: new Map(tags.map((tag) => [tag.key, tag])), reasonCodes: uniqueStrings(reasonCodes) };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseOpeningTag(sourceText, start) {
|
|
164
|
+
const afterOpen = start + 1;
|
|
165
|
+
if (/[/!?>]/.test(sourceText[afterOpen] ?? '')) return undefined;
|
|
166
|
+
const nameMatch = /^[A-Za-z_$][\w$]*(?:[.:][A-Za-z_$][\w$]*|-[\w$]+)*/.exec(sourceText.slice(afterOpen));
|
|
167
|
+
if (!nameMatch) return undefined;
|
|
168
|
+
const tagName = nameMatch[0];
|
|
169
|
+
const nameEnd = afterOpen + tagName.length;
|
|
170
|
+
const end = openingTagEnd(sourceText, nameEnd);
|
|
171
|
+
if (end === undefined) return undefined;
|
|
172
|
+
const attributes = parseAttributes(sourceText, nameEnd, end - 1);
|
|
173
|
+
return {
|
|
174
|
+
tagName,
|
|
175
|
+
start,
|
|
176
|
+
end,
|
|
177
|
+
text: sourceText.slice(start, end),
|
|
178
|
+
attributes: attributes.values,
|
|
179
|
+
reasonCodes: attributes.reasonCodes
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseAttributes(sourceText, start, end) {
|
|
184
|
+
const values = [];
|
|
185
|
+
const reasonCodes = [];
|
|
186
|
+
let cursor = start;
|
|
187
|
+
while (cursor < end) {
|
|
188
|
+
while (cursor < end && /\s/.test(sourceText[cursor])) cursor += 1;
|
|
189
|
+
if (sourceText[cursor] === '/') {
|
|
190
|
+
cursor += 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const attrStart = cursor;
|
|
194
|
+
const nameMatch = /^[A-Za-z_$][\w$:-]*/.exec(sourceText.slice(cursor, end));
|
|
195
|
+
if (!nameMatch) {
|
|
196
|
+
reasonCodes.push('jsx-attribute-token-unsupported');
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
const name = nameMatch[0];
|
|
200
|
+
cursor += name.length;
|
|
201
|
+
while (cursor < end && /\s/.test(sourceText[cursor])) cursor += 1;
|
|
202
|
+
if (sourceText[cursor] === '=') {
|
|
203
|
+
cursor += 1;
|
|
204
|
+
while (cursor < end && /\s/.test(sourceText[cursor])) cursor += 1;
|
|
205
|
+
cursor = attributeValueEnd(sourceText, cursor, end);
|
|
206
|
+
if (cursor === undefined) return { values, reasonCodes: ['jsx-attribute-value-unterminated'] };
|
|
207
|
+
}
|
|
208
|
+
values.push({ name, start: attrStart, end: cursor, text: sourceText.slice(attrStart, cursor) });
|
|
209
|
+
}
|
|
210
|
+
return { values, reasonCodes: uniqueStrings(reasonCodes) };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function openingTagEnd(sourceText, start) {
|
|
214
|
+
let quote;
|
|
215
|
+
let escaped = false;
|
|
216
|
+
let braceDepth = 0;
|
|
217
|
+
for (let index = start; index < sourceText.length; index += 1) {
|
|
218
|
+
const char = sourceText[index];
|
|
219
|
+
if (quote) {
|
|
220
|
+
if (escaped) escaped = false;
|
|
221
|
+
else if (char === '\\') escaped = true;
|
|
222
|
+
else if (char === quote) quote = undefined;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (char === '"' || char === '\'' || char === '`') quote = char;
|
|
226
|
+
else if (char === '{') braceDepth += 1;
|
|
227
|
+
else if (char === '}') braceDepth = Math.max(0, braceDepth - 1);
|
|
228
|
+
else if (char === '>' && braceDepth === 0) return index + 1;
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function attributeValueEnd(sourceText, start, end) {
|
|
234
|
+
const first = sourceText[start];
|
|
235
|
+
if (first === '"' || first === '\'') return quotedValueEnd(sourceText, start, end, first);
|
|
236
|
+
if (first === '{') return bracedValueEnd(sourceText, start, end);
|
|
237
|
+
let cursor = start;
|
|
238
|
+
while (cursor < end && !/[\s/]/.test(sourceText[cursor])) cursor += 1;
|
|
239
|
+
return cursor;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function quotedValueEnd(sourceText, start, end, quote) {
|
|
243
|
+
let escaped = false;
|
|
244
|
+
for (let cursor = start + 1; cursor < end; cursor += 1) {
|
|
245
|
+
const char = sourceText[cursor];
|
|
246
|
+
if (escaped) escaped = false;
|
|
247
|
+
else if (char === '\\') escaped = true;
|
|
248
|
+
else if (char === quote) return cursor + 1;
|
|
249
|
+
}
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function bracedValueEnd(sourceText, start, end) {
|
|
254
|
+
let depth = 0;
|
|
255
|
+
let quote;
|
|
256
|
+
let escaped = false;
|
|
257
|
+
for (let cursor = start; cursor < end; cursor += 1) {
|
|
258
|
+
const char = sourceText[cursor];
|
|
259
|
+
if (quote) {
|
|
260
|
+
if (escaped) escaped = false;
|
|
261
|
+
else if (char === '\\') escaped = true;
|
|
262
|
+
else if (char === quote) quote = undefined;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (char === '"' || char === '\'' || char === '`') quote = char;
|
|
266
|
+
else if (char === '{') depth += 1;
|
|
267
|
+
else if (char === '}') {
|
|
268
|
+
depth -= 1;
|
|
269
|
+
if (depth === 0) return cursor + 1;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function attributeMap(tag) {
|
|
276
|
+
const byName = new Map();
|
|
277
|
+
const duplicateNames = [];
|
|
278
|
+
for (const attribute of tag.attributes) {
|
|
279
|
+
if (byName.has(attribute.name)) duplicateNames.push(attribute.name);
|
|
280
|
+
byName.set(attribute.name, attribute);
|
|
281
|
+
}
|
|
282
|
+
return { byName, reasonCodes: duplicateNames.length ? ['jsx-attribute-duplicate-name'] : [] };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function sameAttributeNames(left, right) {
|
|
286
|
+
return left.attributes.map((attr) => attr.name).join('\0') === right.attributes.map((attr) => attr.name).join('\0');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function sameTagText(left, right) {
|
|
290
|
+
return String(left?.text ?? '').trim() === String(right?.text ?? '').trim();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function sameAttrText(left, right) {
|
|
294
|
+
return String(left?.text ?? '').trim() === String(right?.text ?? '').trim();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function semanticArtifactGates(artifacts) {
|
|
298
|
+
return [
|
|
299
|
+
gate('semantic-edit-script', artifacts.script?.admission?.status === 'auto-merge-candidate', artifacts.script?.admission?.reasonCodes),
|
|
300
|
+
gate('semantic-edit-projection', artifacts.projection?.status === 'projected', artifacts.projection?.admission?.reasonCodes),
|
|
301
|
+
gate('semantic-edit-replay', artifacts.replay?.status === 'accepted-clean', artifacts.replay?.admission?.reasonCodes),
|
|
302
|
+
gate('semantic-edit-already-applied', artifacts.alreadyAppliedReplay?.status === 'already-applied', artifacts.alreadyAppliedReplay?.admission?.reasonCodes)
|
|
303
|
+
];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function gate(id, passed, reasonCodes = []) {
|
|
307
|
+
return { id, status: passed ? 'passed' : 'blocked', reasonCodes: passed ? [] : uniqueStrings(reasonCodes) };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function blocked(...reasonCodes) {
|
|
311
|
+
return { ok: false, reasonCodes: uniqueStrings(reasonCodes) };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function blockedTag(...reasonCodes) {
|
|
315
|
+
return { status: 'blocked', reasonCodes: uniqueStrings(reasonCodes) };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isString(value) { return typeof value === 'string'; }
|
|
319
|
+
|
|
320
|
+
export { createJsxAttributeSemanticFallbackResult };
|
|
@@ -225,11 +225,13 @@ function renderImportStatement(importInfo, specifiers) {
|
|
|
225
225
|
const clause = [];
|
|
226
226
|
if (importInfo.defaultLocalName) clause.push(importInfo.defaultLocalName);
|
|
227
227
|
if (importInfo.namespaceLocalName) clause.push(`* as ${importInfo.namespaceLocalName}`);
|
|
228
|
-
if (specifiers.length) clause.push(`{ ${specifiers.map(
|
|
228
|
+
if (specifiers.length) clause.push(`{ ${specifiers.map((specifier) => renderImportSpecifier(specifier, importInfo)).join(', ')} }`);
|
|
229
229
|
const importType = importInfo.typeOnly ? 'type ' : '';
|
|
230
230
|
return `import ${importType}${clause.join(', ')} from ${importInfo.quote}${importInfo.moduleSpecifier}${importInfo.quote};`;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
const renderImportSpecifier = (specifier, importInfo) => importSpecifierCanonical(importInfo.typeOnly ? { ...specifier, typeOnly: false } : specifier);
|
|
234
|
+
|
|
233
235
|
function importInsertionText(entries, lineEnding) {
|
|
234
236
|
return entries
|
|
235
237
|
.map((entry) => normalizeLineEndings(entry.text.trimEnd(), lineEnding))
|
|
@@ -28,6 +28,14 @@ function semanticEditFallbackResult(input, topLevelResult) {
|
|
|
28
28
|
if (independentDeletionResult) return independentDeletionResult;
|
|
29
29
|
const topLevelRenameAdmission = analyzeTopLevelRenameAdmission(input, topLevelResult);
|
|
30
30
|
if (topLevelRenameAdmission?.status === 'blocked') {
|
|
31
|
+
if (shouldDeferTopLevelRenamePublicContract(input, topLevelRenameAdmission)) {
|
|
32
|
+
const deferredAdmission = deferredTopLevelRenameAdmission(topLevelRenameAdmission);
|
|
33
|
+
const artifacts = createSemanticEditFallbackArtifacts(input, topLevelResult);
|
|
34
|
+
if (artifacts.status !== 'verified') {
|
|
35
|
+
return semanticEditFallbackBlockedResult(input, topLevelResult, artifacts, deferredAdmission);
|
|
36
|
+
}
|
|
37
|
+
return semanticEditFallbackMergedResult(input, topLevelResult, undefined, artifacts, deferredAdmission);
|
|
38
|
+
}
|
|
31
39
|
return topLevelRenameBlockedResult(input, topLevelResult, topLevelRenameAdmission);
|
|
32
40
|
}
|
|
33
41
|
if (topLevelRenameAdmission?.status === 'candidate') {
|
|
@@ -215,4 +223,37 @@ function semanticEditFallbackBlockedResult(input, topLevelResult, artifacts) {
|
|
|
215
223
|
};
|
|
216
224
|
}
|
|
217
225
|
|
|
226
|
+
function shouldDeferTopLevelRenamePublicContract(input, admission) {
|
|
227
|
+
return input.deferTopLevelRenamePublicExportContractToProjectGraph === true
|
|
228
|
+
&& admission.reasonCodes?.length === 1
|
|
229
|
+
&& admission.reasonCodes.includes('top-level-rename-public-export-contract')
|
|
230
|
+
&& admission.summary?.exported !== true
|
|
231
|
+
&& workerPreservesRenamedExportAlias(input.workerSourceText, admission.summary);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function deferredTopLevelRenameAdmission(admission) {
|
|
235
|
+
return {
|
|
236
|
+
...admission,
|
|
237
|
+
status: 'candidate',
|
|
238
|
+
reasonCodes: ['top-level-rename-public-export-contract-deferred-to-project-graph'],
|
|
239
|
+
summary: {
|
|
240
|
+
...admission.summary,
|
|
241
|
+
deferredToProjectGraph: true,
|
|
242
|
+
reasonCodes: ['top-level-rename-public-export-contract-deferred-to-project-graph']
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function workerPreservesRenamedExportAlias(sourceText, summary) {
|
|
248
|
+
const fromName = summary?.fromName;
|
|
249
|
+
const toName = summary?.toName;
|
|
250
|
+
if (typeof sourceText !== 'string' || !fromName || !toName) return false;
|
|
251
|
+
const exportListPattern = new RegExp(`export\\s*\\{[^}]*\\b${escapeRegExp(toName)}\\s+as\\s+${escapeRegExp(fromName)}\\b[^}]*\\}`);
|
|
252
|
+
return exportListPattern.test(sourceText);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function escapeRegExp(value) {
|
|
256
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
257
|
+
}
|
|
258
|
+
|
|
218
259
|
export { semanticEditFallbackResult };
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createEnumMemberSemanticFallbackResult } from './js-ts-safe-merge-enum-member-fallback.js';
|
|
2
|
+
import { createJsxAttributeSemanticFallbackResult } from './js-ts-safe-merge-jsx-attribute-fallback.js';
|
|
2
3
|
import { createVariableDeclaratorSemanticFallbackResult } from './js-ts-safe-merge-variable-declarator-fallback.js';
|
|
3
4
|
|
|
4
5
|
function createSourceShapeSemanticFallbackResult(input, topLevelResult, stagedFallback) {
|
|
5
6
|
return createVariableDeclaratorSemanticFallbackResult(input, topLevelResult, stagedFallback)
|
|
6
|
-
?? createEnumMemberSemanticFallbackResult(input, topLevelResult, stagedFallback)
|
|
7
|
+
?? createEnumMemberSemanticFallbackResult(input, topLevelResult, stagedFallback)
|
|
8
|
+
?? createJsxAttributeSemanticFallbackResult(input, topLevelResult, stagedFallback);
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export { createSourceShapeSemanticFallbackResult };
|
|
@@ -112,6 +112,7 @@ function mergeProjectFile(file, input, projectId) {
|
|
|
112
112
|
...input,
|
|
113
113
|
...context,
|
|
114
114
|
deferReExportIdentityConflictsToProjectGraph: input.includeProjectGraphDelta === true || input.includeOutputProjectSymbolGraph === true,
|
|
115
|
+
deferTopLevelRenamePublicExportContractToProjectGraph: input.includeProjectGraphDelta === true || input.includeOutputProjectSymbolGraph === true,
|
|
115
116
|
id: `${projectId}_${safeId(file.sourcePath)}`,
|
|
116
117
|
baseSourceText: base,
|
|
117
118
|
workerSourceText: worker,
|
|
@@ -304,7 +305,6 @@ function blockedAdmission(reasonCode) {
|
|
|
304
305
|
}
|
|
305
306
|
|
|
306
307
|
function hashText(text) { return typeof text === 'string' ? hashSemanticValue(text) : undefined; }
|
|
307
|
-
|
|
308
308
|
function stringOrUndefined(value) { return typeof value === 'string' ? value : undefined; }
|
|
309
309
|
|
|
310
310
|
function safeId(value) {
|
package/package.json
CHANGED