Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/next/test/private-to-property.test.ts
13383 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import assert from 'assert';
7
import { suite, test } from 'node:test';
8
import { convertPrivateFields, adjustSourceMap } from '../private-to-property.ts';
9
import { SourceMapConsumer, SourceMapGenerator, type RawSourceMap } from 'source-map';
10
11
suite('convertPrivateFields', () => {
12
13
test('no # characters — quick bail-out', () => {
14
const result = convertPrivateFields('const x = 1; function foo() { return x; }', 'test.js');
15
assert.strictEqual(result.code, 'const x = 1; function foo() { return x; }');
16
assert.strictEqual(result.editCount, 0);
17
assert.strictEqual(result.classCount, 0);
18
assert.strictEqual(result.fieldCount, 0);
19
});
20
21
test('class without private fields — identity', () => {
22
const code = 'class Plain { x = 1; get() { return this.x; } }';
23
const result = convertPrivateFields(code, 'test.js');
24
assert.strictEqual(result.code, code);
25
assert.strictEqual(result.editCount, 0);
26
});
27
28
test('basic private field', () => {
29
const code = 'class Foo { #x = 1; get() { return this.#x; } }';
30
const result = convertPrivateFields(code, 'test.js');
31
assert.ok(!result.code.includes('#x'), 'should not contain #x');
32
assert.ok(result.code.includes('$a'), 'should contain replacement $a');
33
assert.strictEqual(result.classCount, 1);
34
assert.strictEqual(result.fieldCount, 1);
35
assert.strictEqual(result.editCount, 2);
36
});
37
38
test('multiple private fields in one class', () => {
39
const code = 'class Foo { #x = 1; #y = 2; get() { return this.#x + this.#y; } }';
40
const result = convertPrivateFields(code, 'test.js');
41
assert.ok(!result.code.includes('#x'));
42
assert.ok(!result.code.includes('#y'));
43
assert.strictEqual(result.fieldCount, 2);
44
assert.strictEqual(result.editCount, 4);
45
});
46
47
test('inheritance — same private name in parent and child get different replacements', () => {
48
const code = [
49
'class Parent { #a = 1; getA() { return this.#a; } }',
50
'class Child extends Parent { #a = 2; getChildA() { return this.#a; } }',
51
].join('\n');
52
const result = convertPrivateFields(code, 'test.js');
53
assert.ok(!result.code.includes('#a'));
54
assert.ok(result.code.includes('$a'), 'Parent should get $a');
55
assert.ok(result.code.includes('$b'), 'Child should get $b');
56
});
57
58
test('static private field — no clash with inherited public property', () => {
59
const code = [
60
'class MyError extends Error {',
61
' static #name = "MyError";',
62
' check(data) { return data.name !== MyError.#name; }',
63
'}',
64
].join('\n');
65
const result = convertPrivateFields(code, 'test.js');
66
assert.ok(!result.code.includes('#name'));
67
assert.ok(result.code.includes('$a'));
68
assert.ok(result.code.includes('data.name'), 'public property should be preserved');
69
});
70
71
test('private method', () => {
72
const code = [
73
'class Bar {',
74
' #normalize(s) { return s.toLowerCase(); }',
75
' process(s) { return this.#normalize(s); }',
76
'}',
77
].join('\n');
78
const result = convertPrivateFields(code, 'test.js');
79
assert.ok(!result.code.includes('#normalize'));
80
assert.strictEqual(result.fieldCount, 1);
81
});
82
83
test('getter/setter pair', () => {
84
const code = [
85
'class WithAccessors {',
86
' #_val;',
87
' get #val() { return this.#_val; }',
88
' set #val(v) { this.#_val = v; }',
89
' init() { this.#val = 42; }',
90
'}',
91
].join('\n');
92
const result = convertPrivateFields(code, 'test.js');
93
assert.ok(!result.code.includes('#_val'));
94
assert.ok(!result.code.includes('#val'));
95
assert.strictEqual(result.fieldCount, 2);
96
});
97
98
test('nested classes — separate scopes', () => {
99
const code = [
100
'class Outer {',
101
' #x = 1;',
102
' method() {',
103
' class Inner {',
104
' #y = 2;',
105
' foo() { return this.#y; }',
106
' }',
107
' return this.#x;',
108
' }',
109
'}',
110
].join('\n');
111
const result = convertPrivateFields(code, 'test.js');
112
assert.ok(!result.code.includes('#x'));
113
assert.ok(!result.code.includes('#y'));
114
assert.strictEqual(result.classCount, 2);
115
});
116
117
test('nested class accessing outer private field', () => {
118
const code = [
119
'class Outer {',
120
' #x = 1;',
121
' method() {',
122
' class Inner {',
123
' foo(o) { return o.#x; }',
124
' }',
125
' return this.#x;',
126
' }',
127
'}',
128
].join('\n');
129
const result = convertPrivateFields(code, 'test.js');
130
assert.ok(!result.code.includes('#x'));
131
const matches = result.code.match(/\$a/g);
132
assert.strictEqual(matches?.length, 3, 'decl + this.#x + o.#x = 3');
133
});
134
135
test('nested classes — same private name get different replacements', () => {
136
const code = [
137
'class Outer {',
138
' #x = 1;',
139
' m() {',
140
' class Inner {',
141
' #x = 2;',
142
' f() { return this.#x; }',
143
' }',
144
' return this.#x;',
145
' }',
146
'}',
147
].join('\n');
148
const result = convertPrivateFields(code, 'test.js');
149
assert.ok(!result.code.includes('#x'));
150
assert.ok(result.code.includes('$a'), 'Outer.#x → $a');
151
assert.ok(result.code.includes('$b'), 'Inner.#x → $b');
152
});
153
154
test('unrelated classes with same private name', () => {
155
const code = [
156
'class A { #data = 1; get() { return this.#data; } }',
157
'class B { #data = 2; get() { return this.#data; } }',
158
].join('\n');
159
const result = convertPrivateFields(code, 'test.js');
160
assert.ok(!result.code.includes('#data'));
161
assert.ok(result.code.includes('$a'));
162
assert.ok(result.code.includes('$b'));
163
});
164
165
test('cross-instance access', () => {
166
const code = [
167
'class Foo {',
168
' #secret = 42;',
169
' equals(other) { return this.#secret === other.#secret; }',
170
'}',
171
].join('\n');
172
const result = convertPrivateFields(code, 'test.js');
173
assert.ok(!result.code.includes('#secret'));
174
const matches = result.code.match(/\$a/g);
175
assert.strictEqual(matches?.length, 3);
176
});
177
178
test('string containing # is not modified', () => {
179
const code = [
180
'class Foo {',
181
' #x = 1;',
182
' label = "use #x for private";',
183
' get() { return this.#x; }',
184
'}',
185
].join('\n');
186
const result = convertPrivateFields(code, 'test.js');
187
assert.ok(result.code.includes('"use #x for private"'), 'string preserved');
188
assert.ok(!result.code.includes('this.#x'), 'usage replaced');
189
});
190
191
test('#field in expr — brand check uses quoted string', () => {
192
const code = 'class Foo { #brand; static check(x) { if (#brand in x) return true; } }';
193
const result = convertPrivateFields(code, 'test.js');
194
assert.ok(!result.code.includes('#brand'));
195
assert.ok(result.code.includes('\'$a\' in x'), 'quoted string for in-check');
196
});
197
198
test('string #brand in obj is not treated as private field', () => {
199
const code = 'class Foo { #brand = true; isFoo(obj) { return "#brand" in obj; } }';
200
const result = convertPrivateFields(code, 'test.js');
201
assert.ok(result.code.includes('"#brand" in obj'), 'string literal preserved');
202
});
203
204
test('transformed code is valid JavaScript', () => {
205
const code = [
206
'class Base { #id = 0; getId() { return this.#id; } }',
207
'class Derived extends Base { #name; constructor(n) { super(); this.#name = n; } getName() { return this.#name; } }',
208
].join('\n');
209
const result = convertPrivateFields(code, 'test.js');
210
assert.doesNotThrow(() => new Function(result.code));
211
});
212
213
test('transformed code executes correctly', () => {
214
const code = [
215
'class Counter {',
216
' #count = 0;',
217
' increment() { this.#count++; }',
218
' get value() { return this.#count; }',
219
'}',
220
'const c = new Counter();',
221
'c.increment(); c.increment(); c.increment();',
222
'return c.value;',
223
].join('\n');
224
const result = convertPrivateFields(code, 'test.js');
225
assert.strictEqual(new Function(result.code)(), 3);
226
});
227
228
test('transformed code executes correctly with inheritance', () => {
229
const code = [
230
'class Animal {',
231
' #sound;',
232
' constructor(s) { this.#sound = s; }',
233
' speak() { return this.#sound; }',
234
'}',
235
'class Dog extends Animal {',
236
' #tricks = [];',
237
' constructor() { super("woof"); }',
238
' learn(trick) { this.#tricks.push(trick); }',
239
' show() { return this.#tricks.join(","); }',
240
'}',
241
'const d = new Dog();',
242
'd.learn("sit"); d.learn("shake");',
243
'return d.speak() + ":" + d.show();',
244
].join('\n');
245
const result = convertPrivateFields(code, 'test.js');
246
assert.strictEqual(new Function(result.code)(), 'woof:sit,shake');
247
});
248
249
suite('name generation', () => {
250
251
test('generates $a through $Z for 52 fields', () => {
252
const fields = [];
253
const usages = [];
254
for (let i = 0; i < 52; i++) {
255
fields.push(`#f${i};`);
256
usages.push(`this.#f${i}`);
257
}
258
const code = `class Big { ${fields.join(' ')} get() { return ${usages.join(' + ')}; } }`;
259
const result = convertPrivateFields(code, 'test.js');
260
assert.ok(result.code.includes('$a'));
261
assert.ok(result.code.includes('$Z'));
262
assert.strictEqual(result.fieldCount, 52);
263
});
264
265
test('wraps to $aa after $Z', () => {
266
const fields = [];
267
const usages = [];
268
for (let i = 0; i < 53; i++) {
269
fields.push(`#f${i};`);
270
usages.push(`this.#f${i}`);
271
}
272
const code = `class Big { ${fields.join(' ')} get() { return ${usages.join(' + ')}; } }`;
273
const result = convertPrivateFields(code, 'test.js');
274
assert.ok(result.code.includes('$aa'));
275
});
276
});
277
278
test('returns edits array', () => {
279
const code = 'class Foo { #x = 1; get() { return this.#x; } }';
280
const result = convertPrivateFields(code, 'test.js');
281
assert.strictEqual(result.edits.length, 2);
282
// Edits should be sorted by start position
283
assert.ok(result.edits[0].start < result.edits[1].start);
284
// First edit is the declaration #x, second is the usage this.#x
285
assert.strictEqual(result.edits[0].newText, '$a');
286
assert.strictEqual(result.edits[1].newText, '$a');
287
});
288
289
test('no edits when no private fields', () => {
290
const code = 'class Foo { x = 1; }';
291
const result = convertPrivateFields(code, 'test.js');
292
assert.deepStrictEqual(result.edits, []);
293
});
294
295
test('async private method — replacement must not merge with async keyword', async () => {
296
// In minified output, there is no space between `async` and `#method`:
297
// class Foo{async#run(){await Promise.resolve(1)}}
298
// Replacing `#run` with `$a` naively produces `async$a()` which is a
299
// single identifier, not `async $a()`. The `await` inside then becomes
300
// invalid because the method is no longer async.
301
const code = 'class Foo{async#run(){return await Promise.resolve(1)}call(){return this.#run()}}';
302
const result = convertPrivateFields(code, 'test.js');
303
assert.ok(!result.code.includes('#run'), 'should replace #run');
304
// The replacement must NOT fuse with `async` into a single token
305
assert.doesNotThrow(() => new Function(result.code), 'transformed code must be valid JS');
306
// Verify it actually executes (the async method should still work)
307
const exec = new Function(`
308
${result.code}
309
return new Foo().call();
310
`);
311
const val = await exec();
312
assert.strictEqual(val, 1);
313
});
314
315
test('async private method — space inserted in declaration and not in usage', () => {
316
// More readable version: ensure that `async #method()` becomes
317
// `async $a()` (with space), while `this.#method()` becomes
318
// `this.$a()` (no extra space needed since `.` separates tokens).
319
const code = [
320
'class Foo {',
321
' async #doWork() { return await 42; }',
322
' run() { return this.#doWork(); }',
323
'}',
324
].join('\n');
325
const result = convertPrivateFields(code, 'test.js');
326
assert.ok(!result.code.includes('#doWork'), 'should replace #doWork');
327
assert.doesNotThrow(() => new Function(result.code), 'transformed code must be valid JS');
328
});
329
330
test('static async private method — no token fusion', async () => {
331
const code = 'class Foo{static async#init(){return await Promise.resolve(1)}static go(){return Foo.#init()}}';
332
const result = convertPrivateFields(code, 'test.js');
333
assert.doesNotThrow(() => new Function(result.code),
334
'static async private method must produce valid JS, got:\n' + result.code);
335
const exec = new Function(`
336
${result.code}
337
return Foo.go();
338
`);
339
const value = await exec();
340
assert.strictEqual(value, 1);
341
});
342
343
test('heritage clause — extends expression resolves outer private field, not inner', () => {
344
const code = [
345
'class Outer {',
346
' #x = "outer";',
347
' method() {',
348
' return class extends (this.#x, Object) {',
349
' #x = "inner";',
350
' };',
351
' }',
352
'}',
353
].join('\n');
354
const result = convertPrivateFields(code, 'test.js');
355
// Outer.#x → $a (first class scanned), Inner.#x → $b (second)
356
// this.#x in the extends clause lexically refers to Outer.#x,
357
// so it must become this.$a, NOT this.$b
358
assert.ok(result.code.includes('this.$a, Object'),
359
'heritage clause should reference outer replacement ($a), got:\n' + result.code);
360
});
361
362
test('heritage clause runtime — extends uses correct outer private field', () => {
363
const code = [
364
'class Base { }',
365
'class Outer {',
366
' #Base = Base;',
367
' createInner() {',
368
' return class extends this.#Base {',
369
' #Base = null;',
370
' };',
371
' }',
372
'}',
373
'const o = new Outer();',
374
'const Inner = o.createInner();',
375
'const inst = new Inner();',
376
'return inst instanceof Base;',
377
].join('\n');
378
const result = convertPrivateFields(code, 'test.js');
379
// With the bug, this.#Base in extends resolves to Inner's replacement
380
// ($b) instead of Outer's ($a). Since the Outer instance has no $b
381
// property, `class extends undefined` throws TypeError.
382
assert.strictEqual(new Function(result.code)(), true,
383
'inner class should extend Base via outer private field, code:\n' + result.code);
384
});
385
386
test('generated name must not collide with existing public property', () => {
387
const code = [
388
'class Foo {',
389
' $a = "public";',
390
' #x = "private";',
391
' getPublic() { return this.$a; }',
392
' getPrivate() { return this.#x; }',
393
'}',
394
].join('\n');
395
const result = convertPrivateFields(code, 'test.js');
396
// #x must not be renamed to $a since the class already has a public $a
397
const fieldDecls = result.code.match(/\$a\s*=/g);
398
assert.ok(!fieldDecls || fieldDecls.length <= 1,
399
'should not produce duplicate $a property declarations, got:\n' + result.code);
400
});
401
402
test('collision with existing property — runtime correctness', () => {
403
const code = [
404
'class Foo {',
405
' $a = "public";',
406
' #x = "private";',
407
' getPublic() { return this.$a; }',
408
' getPrivate() { return this.#x; }',
409
'}',
410
'const f = new Foo();',
411
'return f.getPublic() + "," + f.getPrivate();',
412
].join('\n');
413
const result = convertPrivateFields(code, 'test.js');
414
// Original: getPublic() → "public", getPrivate() → "private"
415
// With the bug: both return "private" because $a overwrites $a
416
assert.strictEqual(new Function(result.code)(), 'public,private',
417
'public and private properties must remain distinct, code:\n' + result.code);
418
});
419
420
test('collision avoidance — string-literal public property name', () => {
421
const code = [
422
'class Foo {',
423
' \'$a\' = "public";',
424
' #x = "private";',
425
' getPublic() { return this[\'$a\']; }',
426
' getPrivate() { return this.#x; }',
427
'}',
428
'const f = new Foo();',
429
'return f.getPublic() + "," + f.getPrivate();',
430
].join('\n');
431
const result = convertPrivateFields(code, 'test.js');
432
assert.strictEqual(new Function(result.code)(), 'public,private',
433
'string-literal public property must not collide, code:\n' + result.code);
434
});
435
436
test('collision avoidance — computed string-literal public property name', () => {
437
const code = [
438
'class Foo {',
439
' [\'$a\'] = "public";',
440
' #x = "private";',
441
' getPublic() { return this[\'$a\']; }',
442
' getPrivate() { return this.#x; }',
443
'}',
444
'const f = new Foo();',
445
'return f.getPublic() + "," + f.getPrivate();',
446
].join('\n');
447
const result = convertPrivateFields(code, 'test.js');
448
assert.strictEqual(new Function(result.code)(), 'public,private',
449
'computed string-literal public property must not collide, code:\n' + result.code);
450
});
451
452
test('brand check in heritage clause resolves to outer scope', () => {
453
const code = [
454
'class Outer {',
455
' #brand;',
456
' createChecked(obj) {',
457
' return class extends (#brand in obj ? Object : Object) {',
458
' #brand;',
459
' };',
460
' }',
461
'}',
462
].join('\n');
463
const result = convertPrivateFields(code, 'test.js');
464
// #brand in the extends clause should resolve to Outer.#brand ($a),
465
// not Inner.#brand ($b)
466
assert.ok(result.code.includes('\'$a\' in obj'),
467
'brand check in heritage clause should use outer replacement, got:\n' + result.code);
468
});
469
});
470
471
suite('adjustSourceMap', () => {
472
473
/**
474
* Helper: creates a source map with dense 1:1 mappings (every character)
475
* for a single-source file. Each column maps generated -> original identity.
476
*/
477
function createIdentitySourceMap(code: string, sourceName: string): RawSourceMap {
478
const gen = new SourceMapGenerator();
479
gen.setSourceContent(sourceName, code);
480
const lines = code.split('\n');
481
for (let line = 0; line < lines.length; line++) {
482
for (let col = 0; col < lines[line].length; col++) {
483
gen.addMapping({
484
generated: { line: line + 1, column: col },
485
original: { line: line + 1, column: col },
486
source: sourceName,
487
});
488
}
489
}
490
return JSON.parse(gen.toString());
491
}
492
493
test('no edits - returns mappings unchanged', () => {
494
const code = 'class Foo { x = 1; }';
495
const map = createIdentitySourceMap(code, 'test.js');
496
const originalMappings = map.mappings;
497
const result = adjustSourceMap(map, code, []);
498
assert.strictEqual(result.mappings, originalMappings);
499
});
500
501
test('single edit shrinks token - columns after edit shift left', () => {
502
// "var #longName = 1; var y = 2;"
503
// 0 4 14 22
504
// After: "var $a = 1; var y = 2;"
505
// 0 4 7 15
506
const code = 'var #longName = 1; var y = 2;';
507
// Create a sparse map with mappings only at known token positions
508
const gen = new SourceMapGenerator();
509
gen.setSourceContent('test.js', code);
510
// Map 'var' at col 0
511
gen.addMapping({ generated: { line: 1, column: 0 }, original: { line: 1, column: 0 }, source: 'test.js' });
512
// Map '#longName' at col 4
513
gen.addMapping({ generated: { line: 1, column: 4 }, original: { line: 1, column: 4 }, source: 'test.js' });
514
// Map '=' at col 14
515
gen.addMapping({ generated: { line: 1, column: 14 }, original: { line: 1, column: 14 }, source: 'test.js' });
516
// Map 'var' at col 19
517
gen.addMapping({ generated: { line: 1, column: 19 }, original: { line: 1, column: 19 }, source: 'test.js' });
518
// Map 'y' at col 23
519
gen.addMapping({ generated: { line: 1, column: 23 }, original: { line: 1, column: 23 }, source: 'test.js' });
520
const map = JSON.parse(gen.toString());
521
522
const result = adjustSourceMap(map, code, [{ start: 4, end: 13, newText: '$a' }]);
523
524
const consumer = new SourceMapConsumer(result);
525
// 'y' was at gen col 23, edit shrunk 9->2 chars (delta -7), so now at gen col 16
526
const pos = consumer.originalPositionFor({ line: 1, column: 16 });
527
assert.strictEqual(pos.column, 23, 'y should map back to original column 23');
528
529
// '=' was at gen col 14, edit shrunk by 7, so now at gen col 7
530
const pos2 = consumer.originalPositionFor({ line: 1, column: 7 });
531
assert.strictEqual(pos2.column, 14, '= should map back to original column 14');
532
});
533
534
test('edit on line does not affect other lines', () => {
535
const code = 'class Foo {\n #x = 1;\n get() { return 42; }\n}';
536
const map = createIdentitySourceMap(code, 'test.js');
537
538
const hashPos = code.indexOf('#x');
539
const result = adjustSourceMap(map, code, [{ start: hashPos, end: hashPos + 2, newText: '$a' }]);
540
541
const consumer = new SourceMapConsumer(result);
542
// Line 3 (1-based) should be completely unaffected
543
const pos = consumer.originalPositionFor({ line: 3, column: 0 });
544
assert.strictEqual(pos.line, 3);
545
assert.strictEqual(pos.column, 0);
546
});
547
548
test('multiple edits on same line accumulate shifts', () => {
549
// "this.#aaa + this.#bbb + this.#ccc;"
550
// 0 5 11 17 23 29
551
const code = 'this.#aaa + this.#bbb + this.#ccc;';
552
// Sparse map at token boundaries (not inside edit spans)
553
const gen = new SourceMapGenerator();
554
gen.setSourceContent('test.js', code);
555
gen.addMapping({ generated: { line: 1, column: 0 }, original: { line: 1, column: 0 }, source: 'test.js' }); // 'this'
556
gen.addMapping({ generated: { line: 1, column: 5 }, original: { line: 1, column: 5 }, source: 'test.js' }); // '#aaa'
557
gen.addMapping({ generated: { line: 1, column: 10 }, original: { line: 1, column: 10 }, source: 'test.js' }); // '+'
558
gen.addMapping({ generated: { line: 1, column: 12 }, original: { line: 1, column: 12 }, source: 'test.js' }); // 'this'
559
gen.addMapping({ generated: { line: 1, column: 17 }, original: { line: 1, column: 17 }, source: 'test.js' }); // '#bbb'
560
gen.addMapping({ generated: { line: 1, column: 22 }, original: { line: 1, column: 22 }, source: 'test.js' }); // '+'
561
gen.addMapping({ generated: { line: 1, column: 24 }, original: { line: 1, column: 24 }, source: 'test.js' }); // 'this'
562
gen.addMapping({ generated: { line: 1, column: 29 }, original: { line: 1, column: 29 }, source: 'test.js' }); // '#ccc'
563
gen.addMapping({ generated: { line: 1, column: 33 }, original: { line: 1, column: 33 }, source: 'test.js' }); // ';'
564
const map = JSON.parse(gen.toString());
565
566
const edits = [
567
{ start: 5, end: 9, newText: '$a' }, // #aaa(4) -> $a(2), delta -2
568
{ start: 17, end: 21, newText: '$b' }, // #bbb(4) -> $b(2), delta -2
569
{ start: 29, end: 33, newText: '$c' }, // #ccc(4) -> $c(2), delta -2
570
];
571
const result = adjustSourceMap(map, code, edits);
572
573
const consumer = new SourceMapConsumer(result);
574
// After edits: "this.$a + this.$b + this.$c;"
575
// '#ccc' was at gen col 29, now at 29-2-2=25
576
const pos = consumer.originalPositionFor({ line: 1, column: 25 });
577
assert.strictEqual(pos.column, 29, 'third edit position should map to original column');
578
579
// '+' after #bbb was at gen col 22, both prior edits shift by -2 each: 22-4=18
580
const pos2 = consumer.originalPositionFor({ line: 1, column: 18 });
581
assert.strictEqual(pos2.column, 22, 'plus after second edit should map correctly');
582
});
583
584
test('end-to-end: convertPrivateFields + adjustSourceMap', () => {
585
const code = [
586
'class MyWidget {',
587
' #count = 0;',
588
' increment() { this.#count++; }',
589
' getValue() { return this.#count; }',
590
'}',
591
].join('\n');
592
593
const map = createIdentitySourceMap(code, 'widget.js');
594
const result = convertPrivateFields(code, 'widget.js');
595
596
assert.ok(result.edits.length > 0, 'should have edits');
597
assert.ok(!result.code.includes('#count'), 'should not contain #count');
598
599
// Adjust the source map
600
const adjusted = adjustSourceMap(map, code, result.edits);
601
const consumer = new SourceMapConsumer(adjusted);
602
603
// Find 'getValue' in the edited output and verify it maps back correctly
604
const editedLines = result.code.split('\n');
605
const getValueLine = editedLines.findIndex(l => l.includes('getValue'));
606
assert.ok(getValueLine >= 0, 'should find getValue in edited code');
607
608
const getValueCol = editedLines[getValueLine].indexOf('getValue');
609
const pos = consumer.originalPositionFor({ line: getValueLine + 1, column: getValueCol });
610
611
// getValue was on line 4 (1-based), same column in original
612
const origLines = code.split('\n');
613
const origGetValueCol = origLines[3].indexOf('getValue');
614
assert.strictEqual(pos.line, 4, 'getValue should map to original line 4');
615
assert.strictEqual(pos.column, origGetValueCol, 'getValue column should match original');
616
});
617
618
test('multi-line edit: removing newlines shifts subsequent lines up', () => {
619
// Simulates the NLS scenario: a template literal with embedded newlines
620
// is replaced with `null`, collapsing 3 lines into 1.
621
const code = [
622
'var a = "hello";', // line 0 (0-based)
623
'var b = `line1', // line 1
624
'line2', // line 2
625
'line3`;', // line 3
626
'var c = "world";', // line 4
627
].join('\n');
628
const map = createIdentitySourceMap(code, 'test.js');
629
630
// Replace the template literal `line1\nline2\nline3` with `null`
631
// (keeps `var b = ` and `;` intact)
632
const tplStart = code.indexOf('`line1');
633
const tplEnd = code.indexOf('line3`') + 'line3`'.length;
634
const edits = [{ start: tplStart, end: tplEnd, newText: 'null' }];
635
636
const result = adjustSourceMap(map, code, edits);
637
const consumer = new SourceMapConsumer(result);
638
639
// After edit, code is:
640
// "var a = \"hello\";\nvar b = null;\nvar c = \"world\";"
641
// "var c" was on line 5 (1-based), now on line 3 (1-based) since 2 newlines removed
642
643
// 'var c' at original line 5, col 0 should now map at generated line 3
644
const pos = consumer.originalPositionFor({ line: 3, column: 0 });
645
assert.strictEqual(pos.line, 5, 'var c should map to original line 5');
646
assert.strictEqual(pos.column, 0, 'var c column should be 0');
647
648
// 'var a' on line 1 should be unaffected
649
const posA = consumer.originalPositionFor({ line: 1, column: 0 });
650
assert.strictEqual(posA.line, 1, 'var a should still map to original line 1');
651
});
652
653
test('brand check: #field in obj -> string replacement adjusts map', () => {
654
const code = 'class C { #x; check(o) { return #x in o; } }';
655
const map = createIdentitySourceMap(code, 'test.js');
656
657
const result = convertPrivateFields(code, 'test.js');
658
const adjusted = adjustSourceMap(map, code, result.edits);
659
const consumer = new SourceMapConsumer(adjusted);
660
661
// 'check' method should still map correctly
662
const editedCheckCol = result.code.indexOf('check');
663
const pos = consumer.originalPositionFor({ line: 1, column: editedCheckCol });
664
assert.strictEqual(pos.line, 1);
665
assert.strictEqual(pos.column, code.indexOf('check'));
666
});
667
});
668
669