From a93689c88cd23b9695f46f2c5ba52c7d03cbab24 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= <kraenhansen@users.noreply.github.com>
Date: Mon, 2 Sep 2024 14:06:01 +0200
Subject: [PATCH] Refactored eslint-plugin to use typescript-eslint

---
 common/config/rush/pnpm-lock.yaml      |  99 ++++++++++++++++++++
 eslint-plugin/package.json             |   9 +-
 eslint-plugin/src/SyntaxRule.ts        | 124 +++++++++++++++++++++++++
 eslint-plugin/src/index.ts             | 123 +-----------------------
 eslint-plugin/src/tests/plugin.test.ts |  10 +-
 eslint-plugin/src/utils.ts             |  20 ++++
 eslint-plugin/tsconfig.json            |   4 +-
 7 files changed, 264 insertions(+), 125 deletions(-)
 create mode 100644 eslint-plugin/src/SyntaxRule.ts
 create mode 100644 eslint-plugin/src/utils.ts

diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml
index e1f61fb8..8b9a5fea 100644
--- a/common/config/rush/pnpm-lock.yaml
+++ b/common/config/rush/pnpm-lock.yaml
@@ -66,12 +66,21 @@ importers:
       '@types/node':
         specifier: 14.18.36
         version: 14.18.36
+      '@typescript-eslint/rule-tester':
+        specifier: ~8.3.0
+        version: 8.3.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/utils':
+        specifier: ~8.3.0
+        version: 8.3.0(eslint@8.57.0)(typescript@5.4.5)
       eslint:
         specifier: ~8.57.0
         version: 8.57.0
       eslint-plugin-header:
         specifier: ~3.1.1
         version: 3.1.1(eslint@8.57.0)
+      typescript:
+        specifier: ~5.4.2
+        version: 5.4.5
 
   ../../playground:
     dependencies:
@@ -1714,6 +1723,24 @@ packages:
       - supports-color
     dev: true
 
+  /@typescript-eslint/rule-tester@8.3.0(eslint@8.57.0)(typescript@5.4.5):
+    resolution: {integrity: sha512-ITX1PUjIUZcj0sVpReC41YLNd+BfSEfcWRI4siYAAbjUdTRT5FpT54Uir6ezqS3RGKd5T8D5Yz3I3G80COa56w==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+    dependencies:
+      '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.4.5)
+      '@typescript-eslint/utils': 8.3.0(eslint@8.57.0)(typescript@5.4.5)
+      ajv: 6.12.6
+      eslint: 8.57.0
+      json-stable-stringify-without-jsonify: 1.0.1
+      lodash.merge: 4.6.2
+      semver: 7.6.3
+    transitivePeerDependencies:
+      - supports-color
+      - typescript
+    dev: true
+
   /@typescript-eslint/scope-manager@6.19.1:
     resolution: {integrity: sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==}
     engines: {node: ^16.0.0 || >=18.0.0}
@@ -1722,6 +1749,14 @@ packages:
       '@typescript-eslint/visitor-keys': 6.19.1
     dev: true
 
+  /@typescript-eslint/scope-manager@8.3.0:
+    resolution: {integrity: sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    dependencies:
+      '@typescript-eslint/types': 8.3.0
+      '@typescript-eslint/visitor-keys': 8.3.0
+    dev: true
+
   /@typescript-eslint/type-utils@6.19.1(eslint@8.57.0)(typescript@5.4.5):
     resolution: {integrity: sha512-0vdyld3ecfxJuddDjACUvlAeYNrHP/pDeQk2pWBR2ESeEzQhg52DF53AbI9QCBkYE23lgkhLCZNkHn2hEXXYIg==}
     engines: {node: ^16.0.0 || >=18.0.0}
@@ -1747,6 +1782,11 @@ packages:
     engines: {node: ^16.0.0 || >=18.0.0}
     dev: true
 
+  /@typescript-eslint/types@8.3.0:
+    resolution: {integrity: sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    dev: true
+
   /@typescript-eslint/typescript-estree@6.19.1(typescript@5.4.5):
     resolution: {integrity: sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==}
     engines: {node: ^16.0.0 || >=18.0.0}
@@ -1769,6 +1809,28 @@ packages:
       - supports-color
     dev: true
 
+  /@typescript-eslint/typescript-estree@8.3.0(typescript@5.4.5):
+    resolution: {integrity: sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+    dependencies:
+      '@typescript-eslint/types': 8.3.0
+      '@typescript-eslint/visitor-keys': 8.3.0
+      debug: 4.3.4
+      fast-glob: 3.3.2
+      is-glob: 4.0.3
+      minimatch: 9.0.5
+      semver: 7.6.3
+      ts-api-utils: 1.3.0(typescript@5.4.5)
+      typescript: 5.4.5
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
   /@typescript-eslint/utils@6.19.1(eslint@8.57.0)(typescript@5.4.5):
     resolution: {integrity: sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w==}
     engines: {node: ^16.0.0 || >=18.0.0}
@@ -1788,6 +1850,22 @@ packages:
       - typescript
     dev: true
 
+  /@typescript-eslint/utils@8.3.0(eslint@8.57.0)(typescript@5.4.5):
+    resolution: {integrity: sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+    dependencies:
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+      '@typescript-eslint/scope-manager': 8.3.0
+      '@typescript-eslint/types': 8.3.0
+      '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.4.5)
+      eslint: 8.57.0
+    transitivePeerDependencies:
+      - supports-color
+      - typescript
+    dev: true
+
   /@typescript-eslint/visitor-keys@6.19.1:
     resolution: {integrity: sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==}
     engines: {node: ^16.0.0 || >=18.0.0}
@@ -1796,6 +1874,14 @@ packages:
       eslint-visitor-keys: 3.4.3
     dev: true
 
+  /@typescript-eslint/visitor-keys@8.3.0:
+    resolution: {integrity: sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    dependencies:
+      '@typescript-eslint/types': 8.3.0
+      eslint-visitor-keys: 3.4.3
+    dev: true
+
   /@ungap/structured-clone@1.2.0:
     resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
     dev: true
@@ -5412,6 +5498,13 @@ packages:
       brace-expansion: 2.0.1
     dev: true
 
+  /minimatch@9.0.5:
+    resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+    engines: {node: '>=16 || 14 >=14.17'}
+    dependencies:
+      brace-expansion: 2.0.1
+    dev: true
+
   /minimist@1.2.8:
     resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
     dev: true
@@ -6796,6 +6889,12 @@ packages:
       lru-cache: 6.0.0
     dev: true
 
+  /semver@7.6.3:
+    resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
+    engines: {node: '>=10'}
+    hasBin: true
+    dev: true
+
   /send@0.18.0:
     resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
     engines: {node: '>= 0.8.0'}
diff --git a/eslint-plugin/package.json b/eslint-plugin/package.json
index 3d759e0e..53c74315 100644
--- a/eslint-plugin/package.json
+++ b/eslint-plugin/package.json
@@ -30,7 +30,13 @@
     "@microsoft/tsdoc": "workspace:*",
     "@microsoft/tsdoc-config": "workspace:*"
   },
+  "peerDependencies": {
+    "@typescript-eslint/parser": "^8",
+    "eslint": "^8"
+  },
   "devDependencies": {
+    "@typescript-eslint/rule-tester": "~8.3.0",
+    "@typescript-eslint/utils": "~8.3.0",
     "@rushstack/heft-node-rig": "~2.6.11",
     "@rushstack/heft": "^0.66.13",
     "@types/eslint": "8.40.1",
@@ -38,6 +44,7 @@
     "@types/heft-jest": "1.0.3",
     "@types/node": "14.18.36",
     "eslint": "~8.57.0",
-    "eslint-plugin-header": "~3.1.1"
+    "eslint-plugin-header": "~3.1.1",
+    "typescript": "~5.4.2"
   }
 }
diff --git a/eslint-plugin/src/SyntaxRule.ts b/eslint-plugin/src/SyntaxRule.ts
new file mode 100644
index 00000000..727a703c
--- /dev/null
+++ b/eslint-plugin/src/SyntaxRule.ts
@@ -0,0 +1,124 @@
+// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
+// See LICENSE in the project root for license information.
+
+import { TSDocParser, TextRange, TSDocConfiguration, type ParserContext } from '@microsoft/tsdoc';
+import type { TSDocConfigFile } from '@microsoft/tsdoc-config';
+import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
+
+const tsdocMessageIds: { [x: string]: string } = {};
+
+const defaultTSDocConfiguration: TSDocConfiguration = new TSDocConfiguration();
+defaultTSDocConfiguration.allTsdocMessageIds.forEach((messageId: string) => {
+  tsdocMessageIds[messageId] = `${messageId}: {{unformattedText}}`;
+});
+
+import { Debug } from './Debug';
+import { ConfigCache } from './ConfigCache';
+
+import { configMessages, createRule } from './utils';
+
+export const rule: TSESLint.AnyRuleModule = createRule({
+  name: 'syntax',
+  meta: {
+    messages: {
+      ...configMessages,
+      ...tsdocMessageIds
+    },
+    type: 'problem',
+    docs: {
+      description: 'Validates that TypeScript documentation comments conform to the TSDoc standard',
+      // This package is experimental
+      recommended: false
+    },
+    schema: []
+  },
+  defaultOptions: [],
+  create: (context: TSESLint.RuleContext<string, unknown[]>) => {
+    const sourceFilePath: string = context.getFilename();
+    Debug.log(`Linting: "${sourceFilePath}"`);
+
+    const tsdocConfiguration: TSDocConfiguration = new TSDocConfiguration();
+
+    try {
+      const tsdocConfigFile: TSDocConfigFile = ConfigCache.getForSourceFile(sourceFilePath);
+      if (!tsdocConfigFile.fileNotFound) {
+        if (tsdocConfigFile.hasErrors) {
+          context.report({
+            loc: { line: 1, column: 1 },
+            messageId: 'error-loading-config-file',
+            data: {
+              details: tsdocConfigFile.getErrorSummary()
+            }
+          });
+        }
+
+        try {
+          tsdocConfigFile.configureParser(tsdocConfiguration);
+        } catch (e) {
+          context.report({
+            loc: { line: 1, column: 1 },
+            messageId: 'error-applying-config',
+            data: {
+              details: e.message
+            }
+          });
+        }
+      }
+    } catch (e) {
+      context.report({
+        loc: { line: 1, column: 1 },
+        messageId: 'error-loading-config-file',
+        data: {
+          details: `Unexpected exception: ${e.message}`
+        }
+      });
+    }
+
+    const tsdocParser: TSDocParser = new TSDocParser(tsdocConfiguration);
+
+    const sourceCode: TSESLint.SourceCode = context.sourceCode;
+    const checkCommentBlocks: (node: TSESTree.Program) => void = function (node: TSESTree.Program) {
+      for (const comment of sourceCode.getAllComments()) {
+        if (comment.type !== 'Block') {
+          continue;
+        }
+        if (!comment.range) {
+          continue;
+        }
+
+        const textRange: TextRange = TextRange.fromStringRange(
+          sourceCode.text,
+          comment.range[0],
+          comment.range[1]
+        );
+
+        // Smallest comment is "/***/"
+        if (textRange.length < 5) {
+          continue;
+        }
+        // Make sure it starts with "/**"
+        if (textRange.buffer[textRange.pos + 2] !== '*') {
+          continue;
+        }
+
+        const parserContext: ParserContext = tsdocParser.parseRange(textRange);
+        for (const message of parserContext.log.messages) {
+          context.report({
+            loc: {
+              start: sourceCode.getLocFromIndex(message.textRange.pos),
+              end: sourceCode.getLocFromIndex(message.textRange.end)
+            },
+            messageId: message.messageId,
+            data: {
+              unformattedText: message.unformattedText
+            }
+          });
+        }
+      }
+    };
+
+    return {
+      Program: checkCommentBlocks
+    };
+  }
+});
diff --git a/eslint-plugin/src/index.ts b/eslint-plugin/src/index.ts
index 94b9e2b3..aba79b02 100644
--- a/eslint-plugin/src/index.ts
+++ b/eslint-plugin/src/index.ts
@@ -1,134 +1,19 @@
 // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
 // See LICENSE in the project root for license information.
 
-import type * as eslint from 'eslint';
-import type * as ESTree from 'estree';
-import { TSDocParser, TextRange, TSDocConfiguration, type ParserContext } from '@microsoft/tsdoc';
-import type { TSDocConfigFile } from '@microsoft/tsdoc-config';
+import type { TSESLint } from '@typescript-eslint/utils';
 
-import { Debug } from './Debug';
-import { ConfigCache } from './ConfigCache';
-
-const tsdocMessageIds: { [x: string]: string } = {};
-
-const defaultTSDocConfiguration: TSDocConfiguration = new TSDocConfiguration();
-defaultTSDocConfiguration.allTsdocMessageIds.forEach((messageId: string) => {
-  tsdocMessageIds[messageId] = `${messageId}: {{unformattedText}}`;
-});
+import { rule as syntaxRule } from './SyntaxRule';
 
 interface IPlugin {
-  rules: { [x: string]: eslint.Rule.RuleModule };
+  rules: { [x: string]: TSESLint.AnyRuleModule };
 }
 
 const plugin: IPlugin = {
   rules: {
     // NOTE: The actual ESLint rule name will be "tsdoc/syntax".  It is calculated by deleting "eslint-plugin-"
     // from the NPM package name, and then appending this string.
-    syntax: {
-      meta: {
-        messages: {
-          'error-loading-config-file': 'Error loading TSDoc config file:\n{{details}}',
-          'error-applying-config': 'Error applying TSDoc configuration: {{details}}',
-          ...tsdocMessageIds
-        },
-        type: 'problem',
-        docs: {
-          description: 'Validates that TypeScript documentation comments conform to the TSDoc standard',
-          category: 'Stylistic Issues',
-          // This package is experimental
-          recommended: false,
-          url: 'https://tsdoc.org/pages/packages/eslint-plugin-tsdoc'
-        }
-      },
-      create: (context: eslint.Rule.RuleContext) => {
-        const sourceFilePath: string = context.getFilename();
-        Debug.log(`Linting: "${sourceFilePath}"`);
-
-        const tsdocConfiguration: TSDocConfiguration = new TSDocConfiguration();
-
-        try {
-          const tsdocConfigFile: TSDocConfigFile = ConfigCache.getForSourceFile(sourceFilePath);
-          if (!tsdocConfigFile.fileNotFound) {
-            if (tsdocConfigFile.hasErrors) {
-              context.report({
-                loc: { line: 1, column: 1 },
-                messageId: 'error-loading-config-file',
-                data: {
-                  details: tsdocConfigFile.getErrorSummary()
-                }
-              });
-            }
-
-            try {
-              tsdocConfigFile.configureParser(tsdocConfiguration);
-            } catch (e) {
-              context.report({
-                loc: { line: 1, column: 1 },
-                messageId: 'error-applying-config',
-                data: {
-                  details: e.message
-                }
-              });
-            }
-          }
-        } catch (e) {
-          context.report({
-            loc: { line: 1, column: 1 },
-            messageId: 'error-loading-config-file',
-            data: {
-              details: `Unexpected exception: ${e.message}`
-            }
-          });
-        }
-
-        const tsdocParser: TSDocParser = new TSDocParser(tsdocConfiguration);
-
-        const sourceCode: eslint.SourceCode = context.getSourceCode();
-        const checkCommentBlocks: (node: ESTree.Node) => void = function (node: ESTree.Node) {
-          for (const comment of sourceCode.getAllComments()) {
-            if (comment.type !== 'Block') {
-              continue;
-            }
-            if (!comment.range) {
-              continue;
-            }
-
-            const textRange: TextRange = TextRange.fromStringRange(
-              sourceCode.text,
-              comment.range[0],
-              comment.range[1]
-            );
-
-            // Smallest comment is "/***/"
-            if (textRange.length < 5) {
-              continue;
-            }
-            // Make sure it starts with "/**"
-            if (textRange.buffer[textRange.pos + 2] !== '*') {
-              continue;
-            }
-
-            const parserContext: ParserContext = tsdocParser.parseRange(textRange);
-            for (const message of parserContext.log.messages) {
-              context.report({
-                loc: {
-                  start: sourceCode.getLocFromIndex(message.textRange.pos),
-                  end: sourceCode.getLocFromIndex(message.textRange.end)
-                },
-                messageId: message.messageId,
-                data: {
-                  unformattedText: message.unformattedText
-                }
-              });
-            }
-          }
-        };
-
-        return {
-          Program: checkCommentBlocks
-        };
-      }
-    }
+    syntax: syntaxRule
   }
 };
 
diff --git a/eslint-plugin/src/tests/plugin.test.ts b/eslint-plugin/src/tests/plugin.test.ts
index e94d95f9..f00cd19c 100644
--- a/eslint-plugin/src/tests/plugin.test.ts
+++ b/eslint-plugin/src/tests/plugin.test.ts
@@ -1,15 +1,17 @@
 // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
 // See LICENSE in the project root for license information.
 
-import { RuleTester } from 'eslint';
+import { RuleTester } from '@typescript-eslint/rule-tester';
+
 import * as plugin from '../index';
 
 const ruleTester: RuleTester = new RuleTester({
-  env: {
-    es6: true
+  languageOptions: {
+    ecmaVersion: 6
   }
 });
-ruleTester.run('"tsdoc/syntax" rule', plugin.rules.syntax, {
+
+ruleTester.run('syntax', plugin.rules.syntax, {
   valid: [
     '/**\nA great function!\n */\nfunction foobar() {}\n',
     '/**\nA great class!\n */\nclass FooBar {}\n'
diff --git a/eslint-plugin/src/utils.ts b/eslint-plugin/src/utils.ts
new file mode 100644
index 00000000..0ba489c5
--- /dev/null
+++ b/eslint-plugin/src/utils.ts
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
+// See LICENSE in the project root for license information.
+
+import { ESLintUtils } from '@typescript-eslint/utils';
+
+export const configMessages: { [x: string]: string } = {
+  'error-loading-config-file': 'Error loading TSDoc config file:\n{{details}}',
+  'error-applying-config': 'Error applying TSDoc configuration: {{details}}'
+};
+
+interface ITsdocPluginDocs {
+  description: string;
+  recommended?: boolean;
+  requiresTypeChecking?: boolean;
+}
+
+export const createRule: ReturnType<typeof ESLintUtils.RuleCreator<ITsdocPluginDocs>> =
+  ESLintUtils.RuleCreator<ITsdocPluginDocs>(
+    (name) => `https://tsdoc.org/pages/packages/eslint-plugin-tsdoc/#${name}`
+  );
diff --git a/eslint-plugin/tsconfig.json b/eslint-plugin/tsconfig.json
index 03d4f1eb..6580359c 100644
--- a/eslint-plugin/tsconfig.json
+++ b/eslint-plugin/tsconfig.json
@@ -3,6 +3,8 @@
   "extends": "./node_modules/@rushstack/heft-node-rig/profiles/default/tsconfig-base.json",
   "compilerOptions": {
     "isolatedModules": true,
-    "types": ["heft-jest", "node"]
+    "types": ["heft-jest", "node"],
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext"
   }
 }