Path: blob/main/build/next/test/private-to-property.test.ts
13383 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { suite, test } from 'node:test';7import { convertPrivateFields, adjustSourceMap } from '../private-to-property.ts';8import { SourceMapConsumer, SourceMapGenerator, type RawSourceMap } from 'source-map';910suite('convertPrivateFields', () => {1112test('no # characters — quick bail-out', () => {13const result = convertPrivateFields('const x = 1; function foo() { return x; }', 'test.js');14assert.strictEqual(result.code, 'const x = 1; function foo() { return x; }');15assert.strictEqual(result.editCount, 0);16assert.strictEqual(result.classCount, 0);17assert.strictEqual(result.fieldCount, 0);18});1920test('class without private fields — identity', () => {21const code = 'class Plain { x = 1; get() { return this.x; } }';22const result = convertPrivateFields(code, 'test.js');23assert.strictEqual(result.code, code);24assert.strictEqual(result.editCount, 0);25});2627test('basic private field', () => {28const code = 'class Foo { #x = 1; get() { return this.#x; } }';29const result = convertPrivateFields(code, 'test.js');30assert.ok(!result.code.includes('#x'), 'should not contain #x');31assert.ok(result.code.includes('$a'), 'should contain replacement $a');32assert.strictEqual(result.classCount, 1);33assert.strictEqual(result.fieldCount, 1);34assert.strictEqual(result.editCount, 2);35});3637test('multiple private fields in one class', () => {38const code = 'class Foo { #x = 1; #y = 2; get() { return this.#x + this.#y; } }';39const result = convertPrivateFields(code, 'test.js');40assert.ok(!result.code.includes('#x'));41assert.ok(!result.code.includes('#y'));42assert.strictEqual(result.fieldCount, 2);43assert.strictEqual(result.editCount, 4);44});4546test('inheritance — same private name in parent and child get different replacements', () => {47const code = [48'class Parent { #a = 1; getA() { return this.#a; } }',49'class Child extends Parent { #a = 2; getChildA() { return this.#a; } }',50].join('\n');51const result = convertPrivateFields(code, 'test.js');52assert.ok(!result.code.includes('#a'));53assert.ok(result.code.includes('$a'), 'Parent should get $a');54assert.ok(result.code.includes('$b'), 'Child should get $b');55});5657test('static private field — no clash with inherited public property', () => {58const code = [59'class MyError extends Error {',60' static #name = "MyError";',61' check(data) { return data.name !== MyError.#name; }',62'}',63].join('\n');64const result = convertPrivateFields(code, 'test.js');65assert.ok(!result.code.includes('#name'));66assert.ok(result.code.includes('$a'));67assert.ok(result.code.includes('data.name'), 'public property should be preserved');68});6970test('private method', () => {71const code = [72'class Bar {',73' #normalize(s) { return s.toLowerCase(); }',74' process(s) { return this.#normalize(s); }',75'}',76].join('\n');77const result = convertPrivateFields(code, 'test.js');78assert.ok(!result.code.includes('#normalize'));79assert.strictEqual(result.fieldCount, 1);80});8182test('getter/setter pair', () => {83const code = [84'class WithAccessors {',85' #_val;',86' get #val() { return this.#_val; }',87' set #val(v) { this.#_val = v; }',88' init() { this.#val = 42; }',89'}',90].join('\n');91const result = convertPrivateFields(code, 'test.js');92assert.ok(!result.code.includes('#_val'));93assert.ok(!result.code.includes('#val'));94assert.strictEqual(result.fieldCount, 2);95});9697test('nested classes — separate scopes', () => {98const code = [99'class Outer {',100' #x = 1;',101' method() {',102' class Inner {',103' #y = 2;',104' foo() { return this.#y; }',105' }',106' return this.#x;',107' }',108'}',109].join('\n');110const result = convertPrivateFields(code, 'test.js');111assert.ok(!result.code.includes('#x'));112assert.ok(!result.code.includes('#y'));113assert.strictEqual(result.classCount, 2);114});115116test('nested class accessing outer private field', () => {117const code = [118'class Outer {',119' #x = 1;',120' method() {',121' class Inner {',122' foo(o) { return o.#x; }',123' }',124' return this.#x;',125' }',126'}',127].join('\n');128const result = convertPrivateFields(code, 'test.js');129assert.ok(!result.code.includes('#x'));130const matches = result.code.match(/\$a/g);131assert.strictEqual(matches?.length, 3, 'decl + this.#x + o.#x = 3');132});133134test('nested classes — same private name get different replacements', () => {135const code = [136'class Outer {',137' #x = 1;',138' m() {',139' class Inner {',140' #x = 2;',141' f() { return this.#x; }',142' }',143' return this.#x;',144' }',145'}',146].join('\n');147const result = convertPrivateFields(code, 'test.js');148assert.ok(!result.code.includes('#x'));149assert.ok(result.code.includes('$a'), 'Outer.#x → $a');150assert.ok(result.code.includes('$b'), 'Inner.#x → $b');151});152153test('unrelated classes with same private name', () => {154const code = [155'class A { #data = 1; get() { return this.#data; } }',156'class B { #data = 2; get() { return this.#data; } }',157].join('\n');158const result = convertPrivateFields(code, 'test.js');159assert.ok(!result.code.includes('#data'));160assert.ok(result.code.includes('$a'));161assert.ok(result.code.includes('$b'));162});163164test('cross-instance access', () => {165const code = [166'class Foo {',167' #secret = 42;',168' equals(other) { return this.#secret === other.#secret; }',169'}',170].join('\n');171const result = convertPrivateFields(code, 'test.js');172assert.ok(!result.code.includes('#secret'));173const matches = result.code.match(/\$a/g);174assert.strictEqual(matches?.length, 3);175});176177test('string containing # is not modified', () => {178const code = [179'class Foo {',180' #x = 1;',181' label = "use #x for private";',182' get() { return this.#x; }',183'}',184].join('\n');185const result = convertPrivateFields(code, 'test.js');186assert.ok(result.code.includes('"use #x for private"'), 'string preserved');187assert.ok(!result.code.includes('this.#x'), 'usage replaced');188});189190test('#field in expr — brand check uses quoted string', () => {191const code = 'class Foo { #brand; static check(x) { if (#brand in x) return true; } }';192const result = convertPrivateFields(code, 'test.js');193assert.ok(!result.code.includes('#brand'));194assert.ok(result.code.includes('\'$a\' in x'), 'quoted string for in-check');195});196197test('string #brand in obj is not treated as private field', () => {198const code = 'class Foo { #brand = true; isFoo(obj) { return "#brand" in obj; } }';199const result = convertPrivateFields(code, 'test.js');200assert.ok(result.code.includes('"#brand" in obj'), 'string literal preserved');201});202203test('transformed code is valid JavaScript', () => {204const code = [205'class Base { #id = 0; getId() { return this.#id; } }',206'class Derived extends Base { #name; constructor(n) { super(); this.#name = n; } getName() { return this.#name; } }',207].join('\n');208const result = convertPrivateFields(code, 'test.js');209assert.doesNotThrow(() => new Function(result.code));210});211212test('transformed code executes correctly', () => {213const code = [214'class Counter {',215' #count = 0;',216' increment() { this.#count++; }',217' get value() { return this.#count; }',218'}',219'const c = new Counter();',220'c.increment(); c.increment(); c.increment();',221'return c.value;',222].join('\n');223const result = convertPrivateFields(code, 'test.js');224assert.strictEqual(new Function(result.code)(), 3);225});226227test('transformed code executes correctly with inheritance', () => {228const code = [229'class Animal {',230' #sound;',231' constructor(s) { this.#sound = s; }',232' speak() { return this.#sound; }',233'}',234'class Dog extends Animal {',235' #tricks = [];',236' constructor() { super("woof"); }',237' learn(trick) { this.#tricks.push(trick); }',238' show() { return this.#tricks.join(","); }',239'}',240'const d = new Dog();',241'd.learn("sit"); d.learn("shake");',242'return d.speak() + ":" + d.show();',243].join('\n');244const result = convertPrivateFields(code, 'test.js');245assert.strictEqual(new Function(result.code)(), 'woof:sit,shake');246});247248suite('name generation', () => {249250test('generates $a through $Z for 52 fields', () => {251const fields = [];252const usages = [];253for (let i = 0; i < 52; i++) {254fields.push(`#f${i};`);255usages.push(`this.#f${i}`);256}257const code = `class Big { ${fields.join(' ')} get() { return ${usages.join(' + ')}; } }`;258const result = convertPrivateFields(code, 'test.js');259assert.ok(result.code.includes('$a'));260assert.ok(result.code.includes('$Z'));261assert.strictEqual(result.fieldCount, 52);262});263264test('wraps to $aa after $Z', () => {265const fields = [];266const usages = [];267for (let i = 0; i < 53; i++) {268fields.push(`#f${i};`);269usages.push(`this.#f${i}`);270}271const code = `class Big { ${fields.join(' ')} get() { return ${usages.join(' + ')}; } }`;272const result = convertPrivateFields(code, 'test.js');273assert.ok(result.code.includes('$aa'));274});275});276277test('returns edits array', () => {278const code = 'class Foo { #x = 1; get() { return this.#x; } }';279const result = convertPrivateFields(code, 'test.js');280assert.strictEqual(result.edits.length, 2);281// Edits should be sorted by start position282assert.ok(result.edits[0].start < result.edits[1].start);283// First edit is the declaration #x, second is the usage this.#x284assert.strictEqual(result.edits[0].newText, '$a');285assert.strictEqual(result.edits[1].newText, '$a');286});287288test('no edits when no private fields', () => {289const code = 'class Foo { x = 1; }';290const result = convertPrivateFields(code, 'test.js');291assert.deepStrictEqual(result.edits, []);292});293294test('async private method — replacement must not merge with async keyword', async () => {295// In minified output, there is no space between `async` and `#method`:296// class Foo{async#run(){await Promise.resolve(1)}}297// Replacing `#run` with `$a` naively produces `async$a()` which is a298// single identifier, not `async $a()`. The `await` inside then becomes299// invalid because the method is no longer async.300const code = 'class Foo{async#run(){return await Promise.resolve(1)}call(){return this.#run()}}';301const result = convertPrivateFields(code, 'test.js');302assert.ok(!result.code.includes('#run'), 'should replace #run');303// The replacement must NOT fuse with `async` into a single token304assert.doesNotThrow(() => new Function(result.code), 'transformed code must be valid JS');305// Verify it actually executes (the async method should still work)306const exec = new Function(`307${result.code}308return new Foo().call();309`);310const val = await exec();311assert.strictEqual(val, 1);312});313314test('async private method — space inserted in declaration and not in usage', () => {315// More readable version: ensure that `async #method()` becomes316// `async $a()` (with space), while `this.#method()` becomes317// `this.$a()` (no extra space needed since `.` separates tokens).318const code = [319'class Foo {',320' async #doWork() { return await 42; }',321' run() { return this.#doWork(); }',322'}',323].join('\n');324const result = convertPrivateFields(code, 'test.js');325assert.ok(!result.code.includes('#doWork'), 'should replace #doWork');326assert.doesNotThrow(() => new Function(result.code), 'transformed code must be valid JS');327});328329test('static async private method — no token fusion', async () => {330const code = 'class Foo{static async#init(){return await Promise.resolve(1)}static go(){return Foo.#init()}}';331const result = convertPrivateFields(code, 'test.js');332assert.doesNotThrow(() => new Function(result.code),333'static async private method must produce valid JS, got:\n' + result.code);334const exec = new Function(`335${result.code}336return Foo.go();337`);338const value = await exec();339assert.strictEqual(value, 1);340});341342test('heritage clause — extends expression resolves outer private field, not inner', () => {343const code = [344'class Outer {',345' #x = "outer";',346' method() {',347' return class extends (this.#x, Object) {',348' #x = "inner";',349' };',350' }',351'}',352].join('\n');353const result = convertPrivateFields(code, 'test.js');354// Outer.#x → $a (first class scanned), Inner.#x → $b (second)355// this.#x in the extends clause lexically refers to Outer.#x,356// so it must become this.$a, NOT this.$b357assert.ok(result.code.includes('this.$a, Object'),358'heritage clause should reference outer replacement ($a), got:\n' + result.code);359});360361test('heritage clause runtime — extends uses correct outer private field', () => {362const code = [363'class Base { }',364'class Outer {',365' #Base = Base;',366' createInner() {',367' return class extends this.#Base {',368' #Base = null;',369' };',370' }',371'}',372'const o = new Outer();',373'const Inner = o.createInner();',374'const inst = new Inner();',375'return inst instanceof Base;',376].join('\n');377const result = convertPrivateFields(code, 'test.js');378// With the bug, this.#Base in extends resolves to Inner's replacement379// ($b) instead of Outer's ($a). Since the Outer instance has no $b380// property, `class extends undefined` throws TypeError.381assert.strictEqual(new Function(result.code)(), true,382'inner class should extend Base via outer private field, code:\n' + result.code);383});384385test('generated name must not collide with existing public property', () => {386const code = [387'class Foo {',388' $a = "public";',389' #x = "private";',390' getPublic() { return this.$a; }',391' getPrivate() { return this.#x; }',392'}',393].join('\n');394const result = convertPrivateFields(code, 'test.js');395// #x must not be renamed to $a since the class already has a public $a396const fieldDecls = result.code.match(/\$a\s*=/g);397assert.ok(!fieldDecls || fieldDecls.length <= 1,398'should not produce duplicate $a property declarations, got:\n' + result.code);399});400401test('collision with existing property — runtime correctness', () => {402const code = [403'class Foo {',404' $a = "public";',405' #x = "private";',406' getPublic() { return this.$a; }',407' getPrivate() { return this.#x; }',408'}',409'const f = new Foo();',410'return f.getPublic() + "," + f.getPrivate();',411].join('\n');412const result = convertPrivateFields(code, 'test.js');413// Original: getPublic() → "public", getPrivate() → "private"414// With the bug: both return "private" because $a overwrites $a415assert.strictEqual(new Function(result.code)(), 'public,private',416'public and private properties must remain distinct, code:\n' + result.code);417});418419test('collision avoidance — string-literal public property name', () => {420const code = [421'class Foo {',422' \'$a\' = "public";',423' #x = "private";',424' getPublic() { return this[\'$a\']; }',425' getPrivate() { return this.#x; }',426'}',427'const f = new Foo();',428'return f.getPublic() + "," + f.getPrivate();',429].join('\n');430const result = convertPrivateFields(code, 'test.js');431assert.strictEqual(new Function(result.code)(), 'public,private',432'string-literal public property must not collide, code:\n' + result.code);433});434435test('collision avoidance — computed string-literal public property name', () => {436const code = [437'class Foo {',438' [\'$a\'] = "public";',439' #x = "private";',440' getPublic() { return this[\'$a\']; }',441' getPrivate() { return this.#x; }',442'}',443'const f = new Foo();',444'return f.getPublic() + "," + f.getPrivate();',445].join('\n');446const result = convertPrivateFields(code, 'test.js');447assert.strictEqual(new Function(result.code)(), 'public,private',448'computed string-literal public property must not collide, code:\n' + result.code);449});450451test('brand check in heritage clause resolves to outer scope', () => {452const code = [453'class Outer {',454' #brand;',455' createChecked(obj) {',456' return class extends (#brand in obj ? Object : Object) {',457' #brand;',458' };',459' }',460'}',461].join('\n');462const result = convertPrivateFields(code, 'test.js');463// #brand in the extends clause should resolve to Outer.#brand ($a),464// not Inner.#brand ($b)465assert.ok(result.code.includes('\'$a\' in obj'),466'brand check in heritage clause should use outer replacement, got:\n' + result.code);467});468});469470suite('adjustSourceMap', () => {471472/**473* Helper: creates a source map with dense 1:1 mappings (every character)474* for a single-source file. Each column maps generated -> original identity.475*/476function createIdentitySourceMap(code: string, sourceName: string): RawSourceMap {477const gen = new SourceMapGenerator();478gen.setSourceContent(sourceName, code);479const lines = code.split('\n');480for (let line = 0; line < lines.length; line++) {481for (let col = 0; col < lines[line].length; col++) {482gen.addMapping({483generated: { line: line + 1, column: col },484original: { line: line + 1, column: col },485source: sourceName,486});487}488}489return JSON.parse(gen.toString());490}491492test('no edits - returns mappings unchanged', () => {493const code = 'class Foo { x = 1; }';494const map = createIdentitySourceMap(code, 'test.js');495const originalMappings = map.mappings;496const result = adjustSourceMap(map, code, []);497assert.strictEqual(result.mappings, originalMappings);498});499500test('single edit shrinks token - columns after edit shift left', () => {501// "var #longName = 1; var y = 2;"502// 0 4 14 22503// After: "var $a = 1; var y = 2;"504// 0 4 7 15505const code = 'var #longName = 1; var y = 2;';506// Create a sparse map with mappings only at known token positions507const gen = new SourceMapGenerator();508gen.setSourceContent('test.js', code);509// Map 'var' at col 0510gen.addMapping({ generated: { line: 1, column: 0 }, original: { line: 1, column: 0 }, source: 'test.js' });511// Map '#longName' at col 4512gen.addMapping({ generated: { line: 1, column: 4 }, original: { line: 1, column: 4 }, source: 'test.js' });513// Map '=' at col 14514gen.addMapping({ generated: { line: 1, column: 14 }, original: { line: 1, column: 14 }, source: 'test.js' });515// Map 'var' at col 19516gen.addMapping({ generated: { line: 1, column: 19 }, original: { line: 1, column: 19 }, source: 'test.js' });517// Map 'y' at col 23518gen.addMapping({ generated: { line: 1, column: 23 }, original: { line: 1, column: 23 }, source: 'test.js' });519const map = JSON.parse(gen.toString());520521const result = adjustSourceMap(map, code, [{ start: 4, end: 13, newText: '$a' }]);522523const consumer = new SourceMapConsumer(result);524// 'y' was at gen col 23, edit shrunk 9->2 chars (delta -7), so now at gen col 16525const pos = consumer.originalPositionFor({ line: 1, column: 16 });526assert.strictEqual(pos.column, 23, 'y should map back to original column 23');527528// '=' was at gen col 14, edit shrunk by 7, so now at gen col 7529const pos2 = consumer.originalPositionFor({ line: 1, column: 7 });530assert.strictEqual(pos2.column, 14, '= should map back to original column 14');531});532533test('edit on line does not affect other lines', () => {534const code = 'class Foo {\n #x = 1;\n get() { return 42; }\n}';535const map = createIdentitySourceMap(code, 'test.js');536537const hashPos = code.indexOf('#x');538const result = adjustSourceMap(map, code, [{ start: hashPos, end: hashPos + 2, newText: '$a' }]);539540const consumer = new SourceMapConsumer(result);541// Line 3 (1-based) should be completely unaffected542const pos = consumer.originalPositionFor({ line: 3, column: 0 });543assert.strictEqual(pos.line, 3);544assert.strictEqual(pos.column, 0);545});546547test('multiple edits on same line accumulate shifts', () => {548// "this.#aaa + this.#bbb + this.#ccc;"549// 0 5 11 17 23 29550const code = 'this.#aaa + this.#bbb + this.#ccc;';551// Sparse map at token boundaries (not inside edit spans)552const gen = new SourceMapGenerator();553gen.setSourceContent('test.js', code);554gen.addMapping({ generated: { line: 1, column: 0 }, original: { line: 1, column: 0 }, source: 'test.js' }); // 'this'555gen.addMapping({ generated: { line: 1, column: 5 }, original: { line: 1, column: 5 }, source: 'test.js' }); // '#aaa'556gen.addMapping({ generated: { line: 1, column: 10 }, original: { line: 1, column: 10 }, source: 'test.js' }); // '+'557gen.addMapping({ generated: { line: 1, column: 12 }, original: { line: 1, column: 12 }, source: 'test.js' }); // 'this'558gen.addMapping({ generated: { line: 1, column: 17 }, original: { line: 1, column: 17 }, source: 'test.js' }); // '#bbb'559gen.addMapping({ generated: { line: 1, column: 22 }, original: { line: 1, column: 22 }, source: 'test.js' }); // '+'560gen.addMapping({ generated: { line: 1, column: 24 }, original: { line: 1, column: 24 }, source: 'test.js' }); // 'this'561gen.addMapping({ generated: { line: 1, column: 29 }, original: { line: 1, column: 29 }, source: 'test.js' }); // '#ccc'562gen.addMapping({ generated: { line: 1, column: 33 }, original: { line: 1, column: 33 }, source: 'test.js' }); // ';'563const map = JSON.parse(gen.toString());564565const edits = [566{ start: 5, end: 9, newText: '$a' }, // #aaa(4) -> $a(2), delta -2567{ start: 17, end: 21, newText: '$b' }, // #bbb(4) -> $b(2), delta -2568{ start: 29, end: 33, newText: '$c' }, // #ccc(4) -> $c(2), delta -2569];570const result = adjustSourceMap(map, code, edits);571572const consumer = new SourceMapConsumer(result);573// After edits: "this.$a + this.$b + this.$c;"574// '#ccc' was at gen col 29, now at 29-2-2=25575const pos = consumer.originalPositionFor({ line: 1, column: 25 });576assert.strictEqual(pos.column, 29, 'third edit position should map to original column');577578// '+' after #bbb was at gen col 22, both prior edits shift by -2 each: 22-4=18579const pos2 = consumer.originalPositionFor({ line: 1, column: 18 });580assert.strictEqual(pos2.column, 22, 'plus after second edit should map correctly');581});582583test('end-to-end: convertPrivateFields + adjustSourceMap', () => {584const code = [585'class MyWidget {',586' #count = 0;',587' increment() { this.#count++; }',588' getValue() { return this.#count; }',589'}',590].join('\n');591592const map = createIdentitySourceMap(code, 'widget.js');593const result = convertPrivateFields(code, 'widget.js');594595assert.ok(result.edits.length > 0, 'should have edits');596assert.ok(!result.code.includes('#count'), 'should not contain #count');597598// Adjust the source map599const adjusted = adjustSourceMap(map, code, result.edits);600const consumer = new SourceMapConsumer(adjusted);601602// Find 'getValue' in the edited output and verify it maps back correctly603const editedLines = result.code.split('\n');604const getValueLine = editedLines.findIndex(l => l.includes('getValue'));605assert.ok(getValueLine >= 0, 'should find getValue in edited code');606607const getValueCol = editedLines[getValueLine].indexOf('getValue');608const pos = consumer.originalPositionFor({ line: getValueLine + 1, column: getValueCol });609610// getValue was on line 4 (1-based), same column in original611const origLines = code.split('\n');612const origGetValueCol = origLines[3].indexOf('getValue');613assert.strictEqual(pos.line, 4, 'getValue should map to original line 4');614assert.strictEqual(pos.column, origGetValueCol, 'getValue column should match original');615});616617test('multi-line edit: removing newlines shifts subsequent lines up', () => {618// Simulates the NLS scenario: a template literal with embedded newlines619// is replaced with `null`, collapsing 3 lines into 1.620const code = [621'var a = "hello";', // line 0 (0-based)622'var b = `line1', // line 1623'line2', // line 2624'line3`;', // line 3625'var c = "world";', // line 4626].join('\n');627const map = createIdentitySourceMap(code, 'test.js');628629// Replace the template literal `line1\nline2\nline3` with `null`630// (keeps `var b = ` and `;` intact)631const tplStart = code.indexOf('`line1');632const tplEnd = code.indexOf('line3`') + 'line3`'.length;633const edits = [{ start: tplStart, end: tplEnd, newText: 'null' }];634635const result = adjustSourceMap(map, code, edits);636const consumer = new SourceMapConsumer(result);637638// After edit, code is:639// "var a = \"hello\";\nvar b = null;\nvar c = \"world\";"640// "var c" was on line 5 (1-based), now on line 3 (1-based) since 2 newlines removed641642// 'var c' at original line 5, col 0 should now map at generated line 3643const pos = consumer.originalPositionFor({ line: 3, column: 0 });644assert.strictEqual(pos.line, 5, 'var c should map to original line 5');645assert.strictEqual(pos.column, 0, 'var c column should be 0');646647// 'var a' on line 1 should be unaffected648const posA = consumer.originalPositionFor({ line: 1, column: 0 });649assert.strictEqual(posA.line, 1, 'var a should still map to original line 1');650});651652test('brand check: #field in obj -> string replacement adjusts map', () => {653const code = 'class C { #x; check(o) { return #x in o; } }';654const map = createIdentitySourceMap(code, 'test.js');655656const result = convertPrivateFields(code, 'test.js');657const adjusted = adjustSourceMap(map, code, result.edits);658const consumer = new SourceMapConsumer(adjusted);659660// 'check' method should still map correctly661const editedCheckCol = result.code.indexOf('check');662const pos = consumer.originalPositionFor({ line: 1, column: editedCheckCol });663assert.strictEqual(pos.line, 1);664assert.strictEqual(pos.column, code.indexOf('check'));665});666});667668669