Repository: ember-cli/eslint-plugin-ember (js)

Issue:
gjs/gts Incorrect token mapping after handlebars
Hi there!

First I would like to say great work on this addon so far, adding `gjs-gts-parser` has enabled us to really do a lot of cool things and increase support for `gjs/gts` files in a lot of tools.

I am currently working on adding `gjs/gts` support to [SonarJS](https://github.com/SonarSource/SonarJS)
https://community.sonarsource.com/t/add-support-for-embers-new-gjs-gts-file-format-to-sonarjs/103769
And it would not be possible without this parser.

I think I have found a bug in the parser while doing this work, but I'm not sure as I'm not really an expert on custom ESLint parsers.
Basically it seems like javascript tokens that come after some handlebars tokens don't have the correct loc mapping.

Here is a test for reproduction
```
  it('correctly maps tokens after handlebars', async () => {
    const eslint = initESLint();
    const code = `
    import Component from '@glimmer/component';

    export const fakeTemplate = <template>
      <div>{{foo}}</div>
    </template>;

    export default class MyComponent extends Component {
      constructor() {
        super(...arguments);
      }

      foo = bar;
      <template>
        <div>
          some totally random, non-meaningful text {{bar}}
        </div>
      </template>
    }
    `;
    const results = await eslint.lintText(code, { filePath: 'my-component.gjs' });

    const resultErrors = results.flatMap((result) => result.messages);
    expect(resultErrors).toHaveLength(3);
    expect(resultErrors[0].message).toBe("'foo' is not defined.");
    expect(resultErrors[0].line).toBe(5);

    expect(resultErrors[1].message).toBe("'bar' is not defined.");
    expect(resultErrors[1].line).toBe(13);

    expect(resultErrors[2].message).toBe("'bar' is not defined.");
    expect(resultErrors[2].line).toBe(16);
  });
```

When we do `foo = bar;` in the test we expect the error to be on line 13, but if you run the actual test output will be line 11, seems like it shifts by the number of lines the `fakeTemplate` is using.

Looking at the code I assume that when we splice template tokens [here](https://github.com/ember-cli/eslint-plugin-ember/blob/master/lib/parsers/gjs-gts-parser.js#L353), we also need to update the `loc` for the tokens that come after the template ones


Pull request:
fix locations for ast after templates
fixes #1993 

Patch:
diff --git a/lib/parsers/gjs-gts-parser.js b/lib/parsers/gjs-gts-parser.js
index 1d437cc1..7b0c41e8 100644
--- a/lib/parsers/gjs-gts-parser.js
+++ b/lib/parsers/gjs-gts-parser.js
@@ -450,20 +450,27 @@ function transformForLint(code) {
    */
   const result = processor.parse(code);
   for (const tplInfo of result.reverse()) {
+    const lineBreaks = [...tplInfo.contents].reduce(
+      (prev, curr) => prev + (DocumentLines.isLineBreak(curr.codePointAt(0)) ? 1 : 0),
+      0
+    );
     if (tplInfo.type === 'class-member') {
       const tplLength = tplInfo.range.end - tplInfo.range.start;
-      const spaces = tplLength - 'static{`'.length - '`}'.length;
-      const total = ' '.repeat(spaces);
+      const spaces = tplLength - 'static{`'.length - '`}'.length - lineBreaks;
+      const total = ' '.repeat(spaces) + '\n'.repeat(lineBreaks);
       const replacementCode = `static{\`${total}\`}`;
       jsCode = replaceRange(jsCode, tplInfo.range.start, tplInfo.range.end, replacementCode);
     } else {
       const tplLength = tplInfo.range.end - tplInfo.range.start;
-      const spaces = tplLength - '`'.length - '`'.length;
-      const total = ' '.repeat(spaces);
+      const spaces = tplLength - '`'.length - '`'.length - lineBreaks;
+      const total = ' '.repeat(spaces) + '\n'.repeat(lineBreaks);
       const replacementCode = `\`${total}\``;
       jsCode = replaceRange(jsCode, tplInfo.range.start, tplInfo.range.end, replacementCode);
     }
   }
+  if (jsCode.length !== code.length) {
+    throw new Error('bad transform');
+  }
   return {
     templateInfos: result,
     output: jsCode,
diff --git a/lib/utils/document.js b/lib/utils/document.js
index 2ae03b8c..d99ff3fc 100644
--- a/lib/utils/document.js
+++ b/lib/utils/document.js
@@ -98,3 +98,4 @@ function isLineBreak(ch) {
 }
 
 module.exports = DocumentLines;
+module.exports.isLineBreak = isLineBreak;


Test patch:
diff --git a/tests/lib/rules-preprocessor/gjs-gts-parser-test.js b/tests/lib/rules-preprocessor/gjs-gts-parser-test.js
index 3d9bb10a..5a89c058 100644
--- a/tests/lib/rules-preprocessor/gjs-gts-parser-test.js
+++ b/tests/lib/rules-preprocessor/gjs-gts-parser-test.js
@@ -688,7 +688,8 @@ describe('multiple tokens in same file', () => {
       severity: 2,
     });
   });
-  it('correctly maps duplicate <template> tokens to the correct lines', async () => {
+
+  it('correctly maps tokens after handlebars', async () => {
     const eslint = initESLint();
     const code = `
     import Component from '@glimmer/component';
@@ -702,7 +703,8 @@ describe('multiple tokens in same file', () => {
         super(...arguments);
       }
 
-      foo = 'bar';
+      foo = bar;
+
       <template>
         <div>
           some totally random, non-meaningful text {{bar}}
@@ -713,11 +715,15 @@ describe('multiple tokens in same file', () => {
     const results = await eslint.lintText(code, { filePath: 'my-component.gjs' });
 
     const resultErrors = results.flatMap((result) => result.messages);
-    expect(resultErrors).toHaveLength(2);
+    expect(resultErrors).toHaveLength(3);
     expect(resultErrors[0].message).toBe("'foo' is not defined.");
     expect(resultErrors[0].line).toBe(5);
 
     expect(resultErrors[1].message).toBe("'bar' is not defined.");
-    expect(resultErrors[1].line).toBe(16);
+    expect(resultErrors[1].endLine).toBe(13);
+    expect(resultErrors[1].line).toBe(13);
+
+    expect(resultErrors[2].message).toBe("'bar' is not defined.");
+    expect(resultErrors[2].line).toBe(17);
   });
 });