Repository: ember-cli/eslint-plugin-ember
Pull request #1852 (2023-05-04 13:03:08)

Only show `no-undef` errors for templates in gts files
this filters out any linting message that is within the template, except for the no-undef rule.
there are also errors like `Expected blank line between class members`, which wrap the whole template block, which will be included

NullVoxPopuli: Can tests be added? Thanks!

Also, is no-undef really the only lint we care about? 😅 
patricklx: Yes, according to this comment: https://github.com/ember-cli/eslint-plugin-ember/blob/208e6b6e8a017f84671891c42953913a5eb05e3f/lib/preprocessors/glimmer.js#L88
NullVoxPopuli: hmm I wrote that. :upside_down_face: 

Sounds good then -- ping when you push up some tests or if you have any questions -- will be good to get this merged!
NullVoxPopuli: Nice, lgtm then. @bmish , can you approve the workflow run?
patricklx: mmm, i will rework this to use the DocumentLines utils from my other PR, which makes the check more easier.
just noticed the check is not completely correct, would be easier to test with offsets.
patricklx: @bmish fixed, tests pass, full coverage
patricklx: Coverage was not enough. I think because of the document utility class.
Can we disable the coverage for the document utility? It has a function which i do not use now. But i use it in the other pr.
I can try to enable it there again 

Patch:
diff --git a/lib/preprocessors/glimmer.js b/lib/preprocessors/glimmer.js
index e68e2ae3..91986cd1 100644
--- a/lib/preprocessors/glimmer.js
+++ b/lib/preprocessors/glimmer.js
@@ -5,6 +5,7 @@ const {
   preprocessEmbeddedTemplates,
 } = require('ember-template-imports/lib/preprocess-embedded-templates');
 const util = require('ember-template-imports/src/util');
+const DocumentLines = require('../utils/document');
 
 const TRANSFORM_CACHE = new Map();
 const TEXT_CACHE = new Map();
@@ -27,6 +28,12 @@ const closingTemplateTagRegex = /`, {.*}\)]/;
 // the capture group would contain `on,noop`
 const getScopeTokenRegex = /scope: \(\) => \({(.*)}\) }\)/;
 
+// eslint rules that we allow for the template parts
+// this are the only rules we want to check within a template
+// Because otherwise eslint would show errors unrelated to the original code and
+// apply auto fixes like spacing, comma etc. into the original template
+const ALLOWED_RULESSET_FOR_TEMPLATES = new Set(['no-undef']);
+
 /**
  * This function is responsible for running the embedded templates transform
  * from ember-template-imports.
@@ -86,6 +93,7 @@ function gjs(text, filename) {
   //
   // We need to disable all of eslint around this "scope" return
   // value and allow-list the rules that check for undefined identifiers
+  // this also ensures that no errors about the generated output appear.
   const transformed = preprocessed.output;
 
   TRANSFORM_CACHE.set(filename, transformed);
@@ -120,9 +128,40 @@ function mapRange(messages, filename) {
     return flattened;
   }
 
+  const transformedDoc = new DocumentLines(transformed);
+
   const lines = transformed.split('\n');
   const originalLines = original.split('\n');
 
+  // get template ranges to remove eslint messages that are inside the templates
+  const templateRanges = [];
+  let isInsideTemplate = false;
+  for (const [index, line] of lines.entries()) {
+    const lineHasOpeningTag = openingTemplateTagRegex.test(line);
+    const lineHasClosingTag = closingTemplateTagRegex.test(line);
+
+    if (lineHasOpeningTag) {
+      isInsideTemplate = true;
+      templateRanges.push({
+        start: transformedDoc.positionToOffset({
+          line: index,
+          character: line.search(openingTemplateTagRegex),
+        }),
+        end: -1,
+      });
+    }
+
+    if (lineHasClosingTag && isInsideTemplate) {
+      isInsideTemplate = false;
+      const match = line.match(closingTemplateTagRegex);
+      const current = templateRanges.slice(-1)[0];
+      current.end = transformedDoc.positionToOffset({
+        line: index,
+        character: match.index + match[0].length,
+      });
+    }
+  }
+
   // this function assumes the original output and transformed output
   // are identical, minus the changes to transform `<template>` into the
   // placeholder `[__GLIMMER_TEMPLATE()]
@@ -135,7 +174,25 @@ function mapRange(messages, filename) {
     );
   }
 
-  return flattened.map((message) => {
+  const filtered = flattened.filter((it) => {
+    const start = transformedDoc.positionToOffset({
+      line: it.line - 1,
+      character: it.column - 1,
+    });
+    const end = transformedDoc.positionToOffset({
+      line: it.endLine - 1,
+      character: it.endColumn - 1,
+    });
+    return (
+      ALLOWED_RULESSET_FOR_TEMPLATES.has(it.ruleId) ||
+      // include if message wraps template
+      templateRanges.some((tpl) => start === tpl.start && end === tpl.end) ||
+      // exclude if message is within template (also when it ends on next line, column 1, so +1 char, e.g semi rule)
+      !templateRanges.some((tpl) => start >= tpl.start && end <= tpl.end + 1)
+    );
+  });
+
+  return filtered.map((message) => {
     // 1. handle eslint diagnostics on single lines.
     if (message.line === message.endLine) {
       const originalLine = originalLines[message.line - 1];
diff --git a/lib/utils/document.js b/lib/utils/document.js
new file mode 100644
index 00000000..c21ac0ea
--- /dev/null
+++ b/lib/utils/document.js
@@ -0,0 +1,101 @@
+/* istanbul ignore file */
+
+/**
+ * @typedef {{ line: number; character: number }} Position
+ */
+
+// Helper class to convert line/column from and to offset
+// taken and adapt from https://github.com/typed-ember/glint/blob/main/packages/core/src/language-server/util/position.ts
+class DocumentLines {
+  /**
+   * @param {string} contents
+   */
+  constructor(contents) {
+    this.lineStarts = computeLineStarts(contents);
+  }
+
+  /**
+   * @param {Position} position
+   * @return {number}
+   */
+  positionToOffset(position) {
+    const { line, character } = position;
+    return this.lineStarts[line] + character;
+  }
+
+  /**
+   *
+   * @param {number} position
+   * @return {Position}
+   */
+  offsetToPosition(position) {
+    const lineStarts = this.lineStarts;
+    let line = 0;
+    while (line + 1 < lineStarts.length && lineStarts[line + 1] <= position) {
+      line++;
+    }
+    const character = position - lineStarts[line];
+    return { line, character };
+  }
+}
+
+/**
+ * @returns {number[]}
+ * @param {string} text
+ */
+function computeLineStarts(text) {
+  const result = [];
+  let pos = 0;
+  let lineStart = 0;
+  while (pos < text.length) {
+    const ch = text.codePointAt(pos);
+    pos++;
+    switch (ch) {
+      case 13 /* carriageReturn */: {
+        if (text.codePointAt(pos) === 10 /* lineFeed */) {
+          pos++;
+        }
+      }
+      // falls through
+      case 10 /* lineFeed */: {
+        result.push(lineStart);
+        lineStart = pos;
+        break;
+      }
+      default: {
+        if (ch > 127 /* maxAsciiCharacter */ && isLineBreak(ch)) {
+          result.push(lineStart);
+          lineStart = pos;
+        }
+        break;
+      }
+    }
+  }
+  result.push(lineStart);
+  return result;
+}
+
+/**
+ * @param {number} ch
+ * @return {boolean}
+ */
+function isLineBreak(ch) {
+  // ES5 7.3:
+  // The ECMAScript line terminator characters are listed in Table 3.
+  //     Table 3: Line Terminator Characters
+  //     Code Unit Value     Name                    Formal Name
+  //     \u000A              Line Feed               <LF>
+  //     \u000D              Carriage Return         <CR>
+  //     \u2028              Line separator          <LS>
+  //     \u2029              Paragraph separator     <PS>
+  // Only the characters in Table 3 are treated as line terminators. Other new line or line
+  // breaking characters are treated as white space but not as line terminators.
+  return (
+    ch === 10 /* lineFeed */ ||
+    ch === 13 /* carriageReturn */ ||
+    ch === 8232 /* lineSeparator */ ||
+    ch === 8233 /* paragraphSeparator */
+  );
+}
+
+module.exports = DocumentLines;


Test patch:
diff --git a/tests/lib/rules-preprocessor/gjs-gts-processor-test.js b/tests/lib/rules-preprocessor/gjs-gts-processor-test.js
index 61c53150..a63607f5 100644
--- a/tests/lib/rules-preprocessor/gjs-gts-processor-test.js
+++ b/tests/lib/rules-preprocessor/gjs-gts-processor-test.js
@@ -35,6 +35,8 @@ function initESLint(options) {
       plugins: ['ember'],
       extends: ['plugin:ember/recommended'],
       rules: {
+        semi: ['error', 'always'],
+        'object-curly-spacing': ['error', 'always'],
         'lines-between-class-members': 'error',
         'no-undef': 'error',
         'no-unused-vars': 'error',
@@ -388,7 +390,7 @@ describe('multiple tokens in same file', () => {
   it('handles duplicate template tokens', async () => {
     const eslint = initESLint();
     const code = `
-      // comment Bad 
+      // comment Bad
 
       const tmpl = <template><Bad /></template>
     `;