From b30d0b14680e03e1689740faae4298b2ac22b601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 2 Sep 2024 13:54:58 +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/NoUndefinedTypesRule.ts | 28 +++++ eslint-plugin/src/SyntaxRule.ts | 125 ++++++++++++++++++++++ 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 +- 8 files changed, 293 insertions(+), 125 deletions(-) create mode 100644 eslint-plugin/src/NoUndefinedTypesRule.ts 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/NoUndefinedTypesRule.ts b/eslint-plugin/src/NoUndefinedTypesRule.ts new file mode 100644 index 00000000..a1ab9616 --- /dev/null +++ b/eslint-plugin/src/NoUndefinedTypesRule.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { TSESLint } from '@typescript-eslint/utils'; + +import { configMessages, createRule } from './utils'; + +export const rule: TSESLint.AnyRuleModule = createRule({ + name: 'no-undefined-types', + defaultOptions: [], + meta: { + messages: { + ...configMessages, + 'error-undefined-reference': 'A TSDoc-comment referenced "{{identifier}}" which is not defined' + }, + type: 'problem', + docs: { + description: 'Validates that TypeScript documentation comments conform to the TSDoc standard', + // This package is experimental + recommended: false, + url: 'https://tsdoc.org/pages/packages/eslint-plugin-tsdoc' + }, + schema: [] + }, + create(context: TSESLint.RuleContext) { + return {}; + } +}); diff --git a/eslint-plugin/src/SyntaxRule.ts b/eslint-plugin/src/SyntaxRule.ts new file mode 100644 index 00000000..708c1d45 --- /dev/null +++ b/eslint-plugin/src/SyntaxRule.ts @@ -0,0 +1,125 @@ +// 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, + url: 'https://tsdoc.org/pages/packages/eslint-plugin-tsdoc' + }, + schema: [] + }, + defaultOptions: [], + create: (context: TSESLint.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: 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> = + ESLintUtils.RuleCreator( + (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" } }