From 370ac729689905384adb20f92240264660fcc9bc Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Wed, 15 May 2019 17:35:52 -0700 Subject: [PATCH] feat: make utils/TSESLint export typed classes instead of just types (#526) --- .eslintrc.json | 7 +- packages/eslint-plugin-tslint/package.json | 3 +- packages/eslint-plugin-tslint/src/index.ts | 159 +--------------- .../eslint-plugin-tslint/src/rules/config.ts | 176 ++++++++++++++++++ .../eslint-plugin-tslint/tests/index.spec.ts | 84 +++++---- packages/eslint-plugin/package.json | 2 +- .../src/configs/eslint-recommended.ts | 2 + .../src/util/getParserServices.ts | 6 +- packages/eslint-plugin/tests/RuleTester.ts | 4 +- .../tests/rules/array-type.test.ts | 4 +- .../tests/rules/await-thenable.test.ts | 12 +- .../no-unnecessary-type-assertion.test.ts | 11 +- .../tests/rules/prefer-function-type.test.ts | 2 +- .../rules/promise-function-async.test.ts | 12 +- .../tests/rules/unified-signatures.test.ts | 2 +- .../eslint-plugin/tools/generate-configs.ts | 9 +- packages/experimental-utils/package.json | 1 + .../src/ts-eslint/CLIEngine.ts | 90 +++++++++ .../src/ts-eslint/Linter.ts | 26 ++- .../src/ts-eslint/ParserOptions.ts | 2 +- .../experimental-utils/src/ts-eslint/Rule.ts | 18 +- .../src/ts-eslint/RuleTester.ts | 9 +- .../src/ts-eslint/SourceCode.ts | 98 +++++----- .../experimental-utils/tsconfig.build.json | 2 +- .../experimental-utils/typings/eslint.d.ts | 15 ++ packages/parser/src/parser-options.ts | 24 +-- packages/parser/tests/lib/basics.ts | 12 +- packages/parser/tests/lib/parser.ts | 5 +- packages/parser/tests/lib/tsx.ts | 4 +- packages/typescript-estree/src/convert.ts | 4 +- .../typescript-estree/src/tsconfig-parser.ts | 2 +- yarn.lock | 15 +- 32 files changed, 479 insertions(+), 343 deletions(-) create mode 100644 packages/eslint-plugin-tslint/src/rules/config.ts create mode 100644 packages/experimental-utils/src/ts-eslint/CLIEngine.ts create mode 100644 packages/experimental-utils/typings/eslint.d.ts diff --git a/.eslintrc.json b/.eslintrc.json index 00e766eaaef..649b9ec2502 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,11 +5,14 @@ "es6": true, "node": true }, - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], "rules": { "comma-dangle": ["error", "always-multiline"], "curly": ["error", "all"], - "no-dupe-class-members": "off", "no-mixed-operators": "error", "no-console": "off", "no-dupe-class-members": "off", diff --git a/packages/eslint-plugin-tslint/package.json b/packages/eslint-plugin-tslint/package.json index b6bfa14be9e..fba222f6dbf 100644 --- a/packages/eslint-plugin-tslint/package.json +++ b/packages/eslint-plugin-tslint/package.json @@ -27,6 +27,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@typescript-eslint/experimental-utils": "1.9.0", "lodash.memoize": "^4.1.2" }, "peerDependencies": { @@ -34,7 +35,7 @@ "tslint": "^5.0.0" }, "devDependencies": { - "@types/eslint": "^4.16.3", + "@types/json-schema": "^7.0.3", "@types/lodash.memoize": "^4.1.4", "@typescript-eslint/parser": "1.9.0" } diff --git a/packages/eslint-plugin-tslint/src/index.ts b/packages/eslint-plugin-tslint/src/index.ts index 46574a106c8..a638ae2ba56 100644 --- a/packages/eslint-plugin-tslint/src/index.ts +++ b/packages/eslint-plugin-tslint/src/index.ts @@ -1,160 +1,9 @@ -import { Rule } from 'eslint'; -import memoize from 'lodash.memoize'; -import { Configuration, RuleSeverity } from 'tslint'; -import { Program } from 'typescript'; -import { CustomLinter } from './custom-linter'; -import { ParserServices } from '@typescript-eslint/typescript-estree'; - -//------------------------------------------------------------------------------ -// Plugin Definition -//------------------------------------------------------------------------------ - -type RawRuleConfig = - | null - | undefined - | boolean - | any[] - | { - severity?: RuleSeverity | 'warn' | 'none' | 'default'; - options?: any; - }; - -interface RawRulesConfig { - [key: string]: RawRuleConfig; -} +import configRule from './rules/config'; /** - * Construct a configFile for TSLint + * Expose a single rule called "config", which will be accessed in the user's eslint config files + * via "tslint/config" */ -const tslintConfig = memoize( - ( - lintFile: string, - tslintRules: RawRulesConfig, - tslintRulesDirectory: string[], - ) => { - if (lintFile != null) { - return Configuration.loadConfigurationFromPath(lintFile); - } - return Configuration.parseConfigFile({ - rules: tslintRules || {}, - rulesDirectory: tslintRulesDirectory || [], - }); - }, - (lintFile: string | undefined, tslintRules = {}, tslintRulesDirectory = []) => - `${lintFile}_${Object.keys(tslintRules).join(',')}_${ - tslintRulesDirectory.length - }`, -); - export const rules = { - /** - * Expose a single rule called "config", which will be accessed in the user's eslint config files - * via "tslint/config" - */ - config: { - meta: { - docs: { - description: - 'Wraps a TSLint configuration and lints the whole source using TSLint', - category: 'TSLint', - }, - schema: [ - { - type: 'object', - properties: { - rules: { - type: 'object', - /** - * No fixed schema properties for rules, as this would be a permanently moving target - */ - additionalProperties: true, - }, - rulesDirectory: { - type: 'array', - items: { - type: 'string', - }, - }, - lintFile: { - type: 'string', - }, - }, - additionalProperties: false, - }, - ], - }, - create(context: Rule.RuleContext) { - const fileName = context.getFilename(); - const sourceCode = context.getSourceCode().text; - const parserServices: ParserServices | undefined = context.parserServices; - - /** - * The user needs to have configured "project" in their parserOptions - * for @typescript-eslint/parser - */ - if (!parserServices || !parserServices.program) { - throw new Error( - `You must provide a value for the "parserOptions.project" property for @typescript-eslint/parser`, - ); - } - - /** - * The TSLint rules configuration passed in by the user - */ - const { - rules: tslintRules, - rulesDirectory: tslintRulesDirectory, - lintFile, - } = context.options[0]; - - const program: Program = parserServices.program; - - /** - * Create an instance of TSLint - * Lint the source code using the configured TSLint instance, and the rules which have been - * passed via the ESLint rule options for this rule (using "tslint/config") - */ - const tslintOptions = { - formatter: 'json', - fix: false, - }; - const tslint = new CustomLinter(tslintOptions, program); - const configuration = tslintConfig( - lintFile, - tslintRules, - tslintRulesDirectory, - ); - tslint.lint(fileName, sourceCode, configuration); - - const result = tslint.getResult(); - - /** - * Format the TSLint results for ESLint - */ - if (result.failures && result.failures.length) { - result.failures.forEach(failure => { - const start = failure.getStartPosition().getLineAndCharacter(); - const end = failure.getEndPosition().getLineAndCharacter(); - context.report({ - message: `${failure.getFailure()} (tslint:${failure.getRuleName()})`, - loc: { - start: { - line: start.line + 1, - column: start.character, - }, - end: { - line: end.line + 1, - column: end.character, - }, - }, - }); - }); - } - - /** - * Return an empty object for the ESLint rule - */ - return {}; - }, - }, + config: configRule, }; diff --git a/packages/eslint-plugin-tslint/src/rules/config.ts b/packages/eslint-plugin-tslint/src/rules/config.ts new file mode 100644 index 00000000000..e9cd3f53bb5 --- /dev/null +++ b/packages/eslint-plugin-tslint/src/rules/config.ts @@ -0,0 +1,176 @@ +import { + ESLintUtils, + ParserServices, +} from '@typescript-eslint/experimental-utils'; +import memoize from 'lodash.memoize'; +import { Configuration, RuleSeverity } from 'tslint'; +import { CustomLinter } from '../custom-linter'; + +// note - cannot migrate this to an import statement because it will make TSC copy the package.json to the dist folder +const version = require('../../package.json').version; + +const createRule = ESLintUtils.RuleCreator( + () => + `https://github.com/typescript-eslint/typescript-eslint/blob/v${version}/packages/eslint-plugin-tslint/README.md`, +); +export type RawRulesConfig = Record< + string, + | null + | undefined + | boolean + | any[] + | { + severity?: RuleSeverity | 'warn' | 'none' | 'default'; + options?: any; + } +>; + +export type MessageIds = 'failure'; +export type Options = [ + { + rules?: RawRulesConfig; + rulesDirectory?: string[]; + lintFile?: string; + } +]; + +/** + * Construct a configFile for TSLint + */ +const tslintConfig = memoize( + ( + lintFile?: string, + tslintRules?: RawRulesConfig, + tslintRulesDirectory?: string[], + ) => { + if (lintFile != null) { + return Configuration.loadConfigurationFromPath(lintFile); + } + return Configuration.parseConfigFile({ + rules: tslintRules || {}, + rulesDirectory: tslintRulesDirectory || [], + }); + }, + (lintFile: string | undefined, tslintRules = {}, tslintRulesDirectory = []) => + `${lintFile}_${Object.keys(tslintRules).join(',')}_${ + tslintRulesDirectory.length + }`, +); + +export default createRule({ + name: 'config', + meta: { + docs: { + description: + 'Wraps a TSLint configuration and lints the whole source using TSLint', + category: 'TSLint' as any, + recommended: false, + }, + type: 'problem', + messages: { + failure: '{{message}} (tslint:{{ruleName}})`', + }, + schema: [ + { + type: 'object', + properties: { + rules: { + type: 'object', + /** + * No fixed schema properties for rules, as this would be a permanently moving target + */ + additionalProperties: true, + }, + rulesDirectory: { + type: 'array', + items: { + type: 'string', + }, + }, + lintFile: { + type: 'string', + }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [] as any, + create(context) { + const fileName = context.getFilename(); + const sourceCode = context.getSourceCode().text; + const parserServices: ParserServices | undefined = context.parserServices; + + /** + * The user needs to have configured "project" in their parserOptions + * for @typescript-eslint/parser + */ + if (!parserServices || !parserServices.program) { + throw new Error( + `You must provide a value for the "parserOptions.project" property for @typescript-eslint/parser`, + ); + } + + /** + * The TSLint rules configuration passed in by the user + */ + const { + rules: tslintRules, + rulesDirectory: tslintRulesDirectory, + lintFile, + } = context.options[0]; + + const program = parserServices.program; + + /** + * Create an instance of TSLint + * Lint the source code using the configured TSLint instance, and the rules which have been + * passed via the ESLint rule options for this rule (using "tslint/config") + */ + const tslintOptions = { + formatter: 'json', + fix: false, + }; + const tslint = new CustomLinter(tslintOptions, program); + const configuration = tslintConfig( + lintFile, + tslintRules, + tslintRulesDirectory, + ); + tslint.lint(fileName, sourceCode, configuration); + + const result = tslint.getResult(); + + /** + * Format the TSLint results for ESLint + */ + if (result.failures && result.failures.length) { + result.failures.forEach(failure => { + const start = failure.getStartPosition().getLineAndCharacter(); + const end = failure.getEndPosition().getLineAndCharacter(); + context.report({ + messageId: 'failure', + data: { + message: failure.getFailure(), + ruleName: failure.getRuleName(), + }, + loc: { + start: { + line: start.line + 1, + column: start.character, + }, + end: { + line: end.line + 1, + column: end.character, + }, + }, + }); + }); + } + + /** + * Return an empty object for the ESLint rule + */ + return {}; + }, +}); diff --git a/packages/eslint-plugin-tslint/tests/index.spec.ts b/packages/eslint-plugin-tslint/tests/index.spec.ts index c62980fb398..516f5a741b4 100644 --- a/packages/eslint-plugin-tslint/tests/index.spec.ts +++ b/packages/eslint-plugin-tslint/tests/index.spec.ts @@ -1,8 +1,8 @@ -import { rules } from '../src'; -import { Linter, RuleTester } from 'eslint'; +import { TSESLint } from '@typescript-eslint/experimental-utils'; import { readFileSync } from 'fs'; +import rule, { Options } from '../src/rules/config'; -const ruleTester = new RuleTester({ +const ruleTester = new TSESLint.RuleTester({ parserOptions: { ecmaVersion: 6, sourceType: 'module', @@ -19,29 +19,33 @@ const ruleTester = new RuleTester({ /** * Inline rules should be supported */ -const tslintRulesConfig = { - rules: { - semicolon: [true, 'always'], +const tslintRulesConfig: Options = [ + { + rules: { + semicolon: [true, 'always'], + }, }, -}; +]; /** * Custom rules directories should be supported */ -const tslintRulesDirectoryConfig = { - rulesDirectory: ['./tests/test-tslint-rules-directory'], - rules: { - 'always-fail': { - severity: 'error', +const tslintRulesDirectoryConfig: Options = [ + { + rulesDirectory: ['./tests/test-tslint-rules-directory'], + rules: { + 'always-fail': { + severity: 'error', + }, }, }, -}; +]; -ruleTester.run('tslint/config', rules.config, { +ruleTester.run('tslint/config', rule, { valid: [ { code: 'var foo = true;', - options: [tslintRulesConfig], + options: tslintRulesConfig, }, { filename: './tests/test-project/file-spec.ts', @@ -52,15 +56,11 @@ ruleTester.run('tslint/config', rules.config, { parserOptions: { project: `${__dirname}/test-project/tsconfig.json`, }, - options: [ - { - ...tslintRulesConfig, - }, - ], + options: tslintRulesConfig, }, { code: 'throw "should be ok because rule is not loaded";', - options: [tslintRulesConfig], + options: tslintRulesConfig, }, ], @@ -70,18 +70,26 @@ ruleTester.run('tslint/config', rules.config, { code: 'throw "err" // no-string-throw', errors: [ { - message: - 'Throwing plain strings (not instances of Error) gives no stack traces (tslint:no-string-throw)', + messageId: 'failure', + data: { + message: + 'Throwing plain strings (not instances of Error) gives no stack traces', + ruleName: 'no-string-throw', + }, }, ], }, { code: 'var foo = true // semicolon', - options: [tslintRulesConfig], + options: tslintRulesConfig, output: 'var foo = true // semicolon', errors: [ { - message: 'Missing semicolon (tslint:semicolon)', + messageId: 'failure', + data: { + message: 'Missing semicolon', + ruleName: 'semicolon', + }, line: 1, column: 15, }, @@ -89,11 +97,15 @@ ruleTester.run('tslint/config', rules.config, { }, { code: 'var foo = true // fail', - options: [tslintRulesDirectoryConfig], + options: tslintRulesDirectoryConfig, output: 'var foo = true // fail', errors: [ { - message: 'failure (tslint:always-fail)', + messageId: 'failure', + data: { + message: 'failure', + ruleName: 'always-fail', + }, line: 1, column: 1, }, @@ -118,8 +130,12 @@ ruleTester.run('tslint/config', rules.config, { ], errors: [ { - message: - 'Operands of \'+\' operation must either be both strings or both numbers, but found 1 + "2". Consider using template literals. (tslint:restrict-plus-operands)', + messageId: 'failure', + data: { + message: + 'Operands of \'+\' operation must either be both strings or both numbers, but found 1 + "2". Consider using template literals.', + ruleName: 'restrict-plus-operands', + }, }, ], }, @@ -127,9 +143,9 @@ ruleTester.run('tslint/config', rules.config, { }); describe('tslint/error', () => { - function testOutput(code: string, config: Linter.Config): void { - const linter = new Linter(); - linter.defineRule('tslint/config', rules.config); + function testOutput(code: string, config: TSESLint.Linter.Config): void { + const linter = new TSESLint.Linter(); + linter.defineRule('tslint/config', rule); expect(() => linter.verify(code, config)).toThrow( `You must provide a value for the "parserOptions.project" property for @typescript-eslint/parser`, @@ -157,9 +173,9 @@ describe('tslint/error', () => { }); it('should not crash if there is no tslint rules specified', () => { - const linter = new Linter(); + const linter = new TSESLint.Linter(); jest.spyOn(console, 'warn').mockImplementation(); - linter.defineRule('tslint/config', rules.config); + linter.defineRule('tslint/config', rule); expect(() => linter.verify('foo;', { parserOptions: { diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 3409c5984c6..a52aacc2352 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -38,7 +38,6 @@ }, "dependencies": { "@typescript-eslint/experimental-utils": "1.9.0", - "@typescript-eslint/parser": "1.9.0", "eslint-utils": "^1.3.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^2.0.1", @@ -48,6 +47,7 @@ "eslint-docs": "^0.2.6" }, "peerDependencies": { + "@typescript-eslint/parser": "1.9.0", "eslint": "^5.0.0" } } diff --git a/packages/eslint-plugin/src/configs/eslint-recommended.ts b/packages/eslint-plugin/src/configs/eslint-recommended.ts index a0f66c7a201..283cd46aa2f 100644 --- a/packages/eslint-plugin/src/configs/eslint-recommended.ts +++ b/packages/eslint-plugin/src/configs/eslint-recommended.ts @@ -34,6 +34,8 @@ export default { 'no-undef': 'off', // This is already checked by Typescript. 'no-dupe-class-members': 'off', + // This is already checked by Typescript. + 'no-redeclare': 'off', /** * 2. Enable more ideomatic code */ diff --git a/packages/eslint-plugin/src/util/getParserServices.ts b/packages/eslint-plugin/src/util/getParserServices.ts index 2cc8b498159..84a9dea9874 100644 --- a/packages/eslint-plugin/src/util/getParserServices.ts +++ b/packages/eslint-plugin/src/util/getParserServices.ts @@ -1,5 +1,7 @@ -import { ParserServices } from '@typescript-eslint/parser'; -import { TSESLint } from '@typescript-eslint/experimental-utils'; +import { + ParserServices, + TSESLint, +} from '@typescript-eslint/experimental-utils'; type RequiredParserServices = { [k in keyof ParserServices]: Exclude diff --git a/packages/eslint-plugin/tests/RuleTester.ts b/packages/eslint-plugin/tests/RuleTester.ts index 4db8bf3909c..34e99972bf7 100644 --- a/packages/eslint-plugin/tests/RuleTester.ts +++ b/packages/eslint-plugin/tests/RuleTester.ts @@ -1,8 +1,8 @@ import { TSESLint, ESLintUtils } from '@typescript-eslint/experimental-utils'; -import { RuleTester as ESLintRuleTester } from 'eslint'; import * as path from 'path'; -const RuleTester: TSESLint.RuleTester = ESLintRuleTester as any; +// re-export the RuleTester from here to make it easier to do the tests +const RuleTester = TSESLint.RuleTester; function getFixturesRootDir() { return path.join(process.cwd(), 'tests/fixtures/'); diff --git a/packages/eslint-plugin/tests/rules/array-type.test.ts b/packages/eslint-plugin/tests/rules/array-type.test.ts index 31d0b018d32..70343f688fc 100644 --- a/packages/eslint-plugin/tests/rules/array-type.test.ts +++ b/packages/eslint-plugin/tests/rules/array-type.test.ts @@ -1,6 +1,6 @@ +import { TSESLint } from '@typescript-eslint/experimental-utils'; import rule from '../../src/rules/array-type'; import { RuleTester } from '../RuleTester'; -import { Linter } from 'eslint'; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', @@ -889,7 +889,7 @@ describe('array-type (nested)', () => { describe('should deeply fix correctly', () => { function testOutput(option: string, code: string, output: string): void { it(code, () => { - const linter = new Linter(); + const linter = new TSESLint.Linter(); linter.defineRule('array-type', Object.assign({}, rule) as any); const result = linter.verifyAndFix( diff --git a/packages/eslint-plugin/tests/rules/await-thenable.test.ts b/packages/eslint-plugin/tests/rules/await-thenable.test.ts index deca521aefa..5ee0f7d0341 100644 --- a/packages/eslint-plugin/tests/rules/await-thenable.test.ts +++ b/packages/eslint-plugin/tests/rules/await-thenable.test.ts @@ -2,16 +2,14 @@ import rule from '../../src/rules/await-thenable'; import { RuleTester, getFixturesRootDir } from '../RuleTester'; const rootDir = getFixturesRootDir(); -const parserOptions = { - ecmaVersion: 2018, - tsconfigRootDir: rootDir, - project: './tsconfig.json', -}; - const messageId = 'await'; const ruleTester = new RuleTester({ - parserOptions, + parserOptions: { + ecmaVersion: 2018, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, parser: '@typescript-eslint/parser', }); diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-type-assertion.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-type-assertion.test.ts index 21428ba7256..7106fc461b4 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-assertion.test.ts @@ -3,13 +3,12 @@ import rule from '../../src/rules/no-unnecessary-type-assertion'; import { RuleTester } from '../RuleTester'; const rootDir = path.join(process.cwd(), 'tests/fixtures'); -const parserOptions = { - ecmaVersion: 2015, - tsconfigRootDir: rootDir, - project: './tsconfig.json', -}; const ruleTester = new RuleTester({ - parserOptions, + parserOptions: { + ecmaVersion: 2015, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, parser: '@typescript-eslint/parser', }); diff --git a/packages/eslint-plugin/tests/rules/prefer-function-type.test.ts b/packages/eslint-plugin/tests/rules/prefer-function-type.test.ts index a472d8619fe..b4c6b4a3de8 100644 --- a/packages/eslint-plugin/tests/rules/prefer-function-type.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-function-type.test.ts @@ -2,7 +2,7 @@ import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; import rule from '../../src/rules/prefer-function-type'; import { RuleTester } from '../RuleTester'; -var ruleTester = new RuleTester({ +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015, }, diff --git a/packages/eslint-plugin/tests/rules/promise-function-async.test.ts b/packages/eslint-plugin/tests/rules/promise-function-async.test.ts index 4f38105989e..3910cea5604 100644 --- a/packages/eslint-plugin/tests/rules/promise-function-async.test.ts +++ b/packages/eslint-plugin/tests/rules/promise-function-async.test.ts @@ -2,16 +2,14 @@ import rule from '../../src/rules/promise-function-async'; import { RuleTester, getFixturesRootDir } from '../RuleTester'; const rootDir = getFixturesRootDir(); -const parserOptions = { - ecmaVersion: 2018, - tsconfigRootDir: rootDir, - project: './tsconfig.json', -}; - const messageId = 'missingAsync'; const ruleTester = new RuleTester({ - parserOptions, + parserOptions: { + ecmaVersion: 2018, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, parser: '@typescript-eslint/parser', }); diff --git a/packages/eslint-plugin/tests/rules/unified-signatures.test.ts b/packages/eslint-plugin/tests/rules/unified-signatures.test.ts index 94b6520de81..5d65e2f1801 100644 --- a/packages/eslint-plugin/tests/rules/unified-signatures.test.ts +++ b/packages/eslint-plugin/tests/rules/unified-signatures.test.ts @@ -5,7 +5,7 @@ import { RuleTester } from '../RuleTester'; // Tests //------------------------------------------------------------------------------ -var ruleTester = new RuleTester({ parser: '@typescript-eslint/parser' }); +const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser' }); ruleTester.run('unified-signatures', rule, { valid: [ diff --git a/packages/eslint-plugin/tools/generate-configs.ts b/packages/eslint-plugin/tools/generate-configs.ts index 4c29753a048..9809adc05c7 100644 --- a/packages/eslint-plugin/tools/generate-configs.ts +++ b/packages/eslint-plugin/tools/generate-configs.ts @@ -1,16 +1,17 @@ /* eslint-disable no-console */ -import { Linter } from 'eslint'; +import { TSESLint } from '@typescript-eslint/experimental-utils'; import fs from 'fs'; import path from 'path'; -import { TSESLint } from '@typescript-eslint/experimental-utils'; import rules from '../src/rules'; interface LinterConfigRules { - [name: string]: Linter.RuleLevel | Linter.RuleLevelAndOptions; + [name: string]: + | TSESLint.Linter.RuleLevel + | TSESLint.Linter.RuleLevelAndOptions; } -interface LinterConfig extends Linter.Config { +interface LinterConfig extends TSESLint.Linter.Config { extends?: string | string[]; plugins?: string[]; } diff --git a/packages/experimental-utils/package.json b/packages/experimental-utils/package.json index 5bafdb42c7d..8d7cb94092f 100644 --- a/packages/experimental-utils/package.json +++ b/packages/experimental-utils/package.json @@ -34,6 +34,7 @@ "@typescript-eslint/typescript-estree": "1.9.0" }, "peerDependencies": { + "eslint": "*", "typescript": "*" } } diff --git a/packages/experimental-utils/src/ts-eslint/CLIEngine.ts b/packages/experimental-utils/src/ts-eslint/CLIEngine.ts new file mode 100644 index 00000000000..0a64a3d6734 --- /dev/null +++ b/packages/experimental-utils/src/ts-eslint/CLIEngine.ts @@ -0,0 +1,90 @@ +/* eslint-disable @typescript-eslint/no-namespace, no-redeclare */ + +import { CLIEngine as ESLintCLIEngine } from 'eslint'; +import { Linter } from './Linter'; +import { RuleModule, RuleListener } from './Rule'; + +interface CLIEngine { + version: string; + + executeOnFiles(patterns: string[]): CLIEngine.LintReport; + + resolveFileGlobPatterns(patterns: string[]): string[]; + + getConfigForFile(filePath: string): Linter.Config; + + executeOnText(text: string, filename?: string): CLIEngine.LintReport; + + addPlugin(name: string, pluginObject: any): void; + + isPathIgnored(filePath: string): boolean; + + getFormatter(format?: string): CLIEngine.Formatter; + + getRules< + TMessageIds extends string = any, + TOptions extends readonly any[] = any[], + // for extending base rules + TRuleListener extends RuleListener = RuleListener + >(): Map>; +} + +namespace CLIEngine { + export interface Options { + allowInlineConfig?: boolean; + baseConfig?: false | { [name: string]: any }; + cache?: boolean; + cacheFile?: string; + cacheLocation?: string; + configFile?: string; + cwd?: string; + envs?: string[]; + extensions?: string[]; + fix?: boolean; + globals?: string[]; + ignore?: boolean; + ignorePath?: string; + ignorePattern?: string | string[]; + useEslintrc?: boolean; + parser?: string; + parserOptions?: Linter.ParserOptions; + plugins?: string[]; + rules?: { + [name: string]: Linter.RuleLevel | Linter.RuleLevelAndOptions; + }; + rulePaths?: string[]; + reportUnusedDisableDirectives?: boolean; + } + + export interface LintResult { + filePath: string; + messages: Linter.LintMessage[]; + errorCount: number; + warningCount: number; + fixableErrorCount: number; + fixableWarningCount: number; + output?: string; + source?: string; + } + + export interface LintReport { + results: LintResult[]; + errorCount: number; + warningCount: number; + fixableErrorCount: number; + fixableWarningCount: number; + } + + export type Formatter = (results: LintResult[]) => string; +} + +const CLIEngine = ESLintCLIEngine as { + new (options: CLIEngine.Options): CLIEngine; + + // static methods + getErrorResults(results: CLIEngine.LintResult[]): CLIEngine.LintResult[]; + + outputFixes(report: CLIEngine.LintReport): void; +}; + +export { CLIEngine }; diff --git a/packages/experimental-utils/src/ts-eslint/Linter.ts b/packages/experimental-utils/src/ts-eslint/Linter.ts index cff921e048c..dde85a07f2b 100644 --- a/packages/experimental-utils/src/ts-eslint/Linter.ts +++ b/packages/experimental-utils/src/ts-eslint/Linter.ts @@ -1,11 +1,13 @@ /* eslint-disable @typescript-eslint/no-namespace, no-redeclare */ import { TSESTree, ParserServices } from '@typescript-eslint/typescript-estree'; +import { Linter as ESLintLinter } from 'eslint'; +import { ParserOptions as TSParserOptions } from './ParserOptions'; import { RuleModule, RuleFix } from './Rule'; import { Scope } from './Scope'; import { SourceCode } from './SourceCode'; -declare class Linter { +interface Linter { version: string; verify( @@ -34,7 +36,10 @@ declare class Linter { defineRule( name: string, - rule: RuleModule, + rule: { + meta?: RuleModule['meta']; + create: RuleModule['create']; + }, ): void; defineRules( @@ -68,18 +73,7 @@ namespace Linter { globals?: { [name: string]: boolean }; } - export interface ParserOptions { - ecmaVersion?: 3 | 5 | 6 | 7 | 8 | 9 | 2015 | 2016 | 2017 | 2018; - sourceType?: 'script' | 'module'; - ecmaFeatures?: { - globalReturn?: boolean; - impliedStrict?: boolean; - jsx?: boolean; - experimentalObjectRestSpread?: boolean; - [key: string]: any; - }; - [key: string]: any; - } + export type ParserOptions = TSParserOptions; export interface LintOptions { filename?: string; @@ -129,4 +123,8 @@ namespace Linter { } } +const Linter = ESLintLinter as { + new (): Linter; +}; + export { Linter }; diff --git a/packages/experimental-utils/src/ts-eslint/ParserOptions.ts b/packages/experimental-utils/src/ts-eslint/ParserOptions.ts index d374ac57b91..915e6726172 100644 --- a/packages/experimental-utils/src/ts-eslint/ParserOptions.ts +++ b/packages/experimental-utils/src/ts-eslint/ParserOptions.ts @@ -4,7 +4,7 @@ export interface ParserOptions { range?: boolean; tokens?: boolean; sourceType?: 'script' | 'module'; - ecmaVersion?: number; + ecmaVersion?: 3 | 5 | 6 | 7 | 8 | 9 | 2015 | 2016 | 2017 | 2018; ecmaFeatures?: { globalReturn?: boolean; jsx?: boolean; diff --git a/packages/experimental-utils/src/ts-eslint/Rule.ts b/packages/experimental-utils/src/ts-eslint/Rule.ts index 48162df0867..388f64e99fc 100644 --- a/packages/experimental-utils/src/ts-eslint/Rule.ts +++ b/packages/experimental-utils/src/ts-eslint/Rule.ts @@ -105,7 +105,7 @@ type ReportFixFunction = ( fixer: RuleFixer, ) => null | RuleFix | RuleFix[] | IterableIterator; -interface ReportDescriptor { +interface ReportDescriptorBase { /** * The parameters for the message string associated with `messageId`. */ @@ -118,6 +118,8 @@ interface ReportDescriptor { * The messageId which is being reported. */ messageId: TMessageIds; +} +interface ReportDescriptorNodeOptionalLoc { /** * The Node or AST Token which the report is being attached to */ @@ -127,10 +129,20 @@ interface ReportDescriptor { */ loc?: TSESTree.SourceLocation | TSESTree.LineAndColumnData; } +interface ReportDescriptorLocOnly { + /** + * An override of the location of the report + */ + loc: TSESTree.SourceLocation | TSESTree.LineAndColumnData; +} +type ReportDescriptor = ReportDescriptorBase< + TMessageIds +> & + (ReportDescriptorNodeOptionalLoc | ReportDescriptorLocOnly); interface RuleContext< TMessageIds extends string, - TOptions extends Readonly + TOptions extends readonly any[] > { /** * The rule ID. @@ -370,7 +382,7 @@ interface RuleListener { interface RuleModule< TMessageIds extends string, - TOptions extends Readonly, + TOptions extends readonly any[], // for extending base rules TRuleListener extends RuleListener = RuleListener > { diff --git a/packages/experimental-utils/src/ts-eslint/RuleTester.ts b/packages/experimental-utils/src/ts-eslint/RuleTester.ts index ea677806cf3..4478abca8dd 100644 --- a/packages/experimental-utils/src/ts-eslint/RuleTester.ts +++ b/packages/experimental-utils/src/ts-eslint/RuleTester.ts @@ -2,6 +2,7 @@ import { AST_NODE_TYPES, AST_TOKEN_TYPES, } from '@typescript-eslint/typescript-estree'; +import { RuleTester as ESLintRuleTester } from 'eslint'; import { ParserOptions } from './ParserOptions'; import { RuleModule } from './Rule'; @@ -57,16 +58,16 @@ interface RuleTesterConfig { parser: '@typescript-eslint/parser'; parserOptions?: ParserOptions; } -interface RuleTester { - // eslint-disable-next-line @typescript-eslint/no-misused-new - new (config?: RuleTesterConfig): RuleTester; - +declare interface RuleTester { run>( name: string, rule: RuleModule, tests: RunTests, ): void; } +const RuleTester = ESLintRuleTester as { + new (config?: RuleTesterConfig): RuleTester; +}; export { InvalidTestCase, diff --git a/packages/experimental-utils/src/ts-eslint/SourceCode.ts b/packages/experimental-utils/src/ts-eslint/SourceCode.ts index abf3c3e6e8f..2fb2e0b3cab 100644 --- a/packages/experimental-utils/src/ts-eslint/SourceCode.ts +++ b/packages/experimental-utils/src/ts-eslint/SourceCode.ts @@ -1,50 +1,10 @@ /* eslint-disable @typescript-eslint/no-namespace, no-redeclare */ import { ParserServices, TSESTree } from '@typescript-eslint/typescript-estree'; +import { SourceCode as ESLintSourceCode } from 'eslint'; import { Scope } from './Scope'; -namespace SourceCode { - export interface Program extends TSESTree.Program { - comments: TSESTree.Comment[]; - tokens: TSESTree.Token[]; - } - - export interface Config { - text: string; - ast: Program; - parserServices?: ParserServices; - scopeManager?: Scope.ScopeManager; - visitorKeys?: VisitorKeys; - } - - export interface VisitorKeys { - [nodeType: string]: string[]; - } - - export type FilterPredicate = ( - tokenOrComment: TSESTree.Token | TSESTree.Comment, - ) => boolean; - - export type CursorWithSkipOptions = - | number - | FilterPredicate - | { - includeComments?: boolean; - filter?: FilterPredicate; - skip?: number; - }; - - export type CursorWithCountOptions = - | number - | FilterPredicate - | { - includeComments?: boolean; - filter?: FilterPredicate; - count?: number; - }; -} - -declare class SourceCode { +declare interface SourceCode { text: string; ast: SourceCode.Program; lines: string[]; @@ -54,11 +14,6 @@ declare class SourceCode { visitorKeys: SourceCode.VisitorKeys; tokensAndComments: (TSESTree.Comment | TSESTree.Token)[]; - constructor(text: string, ast: SourceCode.Program); - constructor(config: SourceCode.Config); - - static splitLines(text: string): string[]; - getText( node?: TSESTree.Node, beforeCount?: number, @@ -190,4 +145,53 @@ declare class SourceCode { getCommentsInside(node: TSESTree.Node): TSESTree.Comment[]; } +namespace SourceCode { + export interface Program extends TSESTree.Program { + comments: TSESTree.Comment[]; + tokens: TSESTree.Token[]; + } + + export interface Config { + text: string; + ast: Program; + parserServices?: ParserServices; + scopeManager?: Scope.ScopeManager; + visitorKeys?: VisitorKeys; + } + + export interface VisitorKeys { + [nodeType: string]: string[]; + } + + export type FilterPredicate = ( + tokenOrComment: TSESTree.Token | TSESTree.Comment, + ) => boolean; + + export type CursorWithSkipOptions = + | number + | FilterPredicate + | { + includeComments?: boolean; + filter?: FilterPredicate; + skip?: number; + }; + + export type CursorWithCountOptions = + | number + | FilterPredicate + | { + includeComments?: boolean; + filter?: FilterPredicate; + count?: number; + }; +} + +const SourceCode = ESLintSourceCode as { + new (text: string, ast: SourceCode.Program): SourceCode; + new (config: SourceCode.Config): SourceCode; + + // static methods + splitLines(text: string): string[]; +}; + export { SourceCode }; diff --git a/packages/experimental-utils/tsconfig.build.json b/packages/experimental-utils/tsconfig.build.json index 0ce1565b0d0..c052e521130 100644 --- a/packages/experimental-utils/tsconfig.build.json +++ b/packages/experimental-utils/tsconfig.build.json @@ -5,5 +5,5 @@ "rootDir": "./src", "resolveJsonModule": true }, - "include": ["src"] + "include": ["src", "typings"] } diff --git a/packages/experimental-utils/typings/eslint.d.ts b/packages/experimental-utils/typings/eslint.d.ts new file mode 100644 index 00000000000..a32b469a977 --- /dev/null +++ b/packages/experimental-utils/typings/eslint.d.ts @@ -0,0 +1,15 @@ +/* +We intentionally do not include @types/eslint. + +This is to ensure that nobody accidentally uses those incorrect types +instead of the ones declared within this package +*/ + +declare module 'eslint' { + const Linter: unknown; + const RuleTester: unknown; + const SourceCode: unknown; + const CLIEngine: unknown; + + export { Linter, RuleTester, SourceCode, CLIEngine }; +} diff --git a/packages/parser/src/parser-options.ts b/packages/parser/src/parser-options.ts index d374ac57b91..9848d54ba70 100644 --- a/packages/parser/src/parser-options.ts +++ b/packages/parser/src/parser-options.ts @@ -1,21 +1,3 @@ -export interface ParserOptions { - loc?: boolean; - comment?: boolean; - range?: boolean; - tokens?: boolean; - sourceType?: 'script' | 'module'; - ecmaVersion?: number; - ecmaFeatures?: { - globalReturn?: boolean; - jsx?: boolean; - }; - // ts-estree specific - filePath?: string; - project?: string | string[]; - useJSXTextNode?: boolean; - errorOnUnknownASTType?: boolean; - errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; - tsconfigRootDir?: string; - extraFileExtensions?: string[]; - warnOnUnsupportedTypeScriptVersion?: boolean; -} +import { TSESLint } from '@typescript-eslint/experimental-utils'; + +export type ParserOptions = TSESLint.ParserOptions; diff --git a/packages/parser/tests/lib/basics.ts b/packages/parser/tests/lib/basics.ts index 042e3fd731c..4b237a7dc2a 100644 --- a/packages/parser/tests/lib/basics.ts +++ b/packages/parser/tests/lib/basics.ts @@ -1,4 +1,4 @@ -import { Linter } from 'eslint'; +import { TSESLint } from '@typescript-eslint/experimental-utils'; import fs from 'fs'; import glob from 'glob'; import * as parser from '../../src/parser'; @@ -24,11 +24,11 @@ describe('basics', () => { }); it('https://github.com/eslint/typescript-eslint-parser/issues/476', () => { - const linter = new Linter(); + const linter = new TSESLint.Linter(); const code = ` export const Price: React.SFC = function Price(props) {} `; - const config: Linter.Config = { + const config: TSESLint.Linter.Config = { parser: '@typescript-eslint/parser', rules: { test: 'error', @@ -37,15 +37,15 @@ export const Price: React.SFC = function Price(props) {} linter.defineParser('@typescript-eslint/parser', parser); linter.defineRule('test', { - create(context: any) { + create(context) { return { - TSTypeReference(node: any) { + TSTypeReference(node) { const name = context.getSourceCode().getText(node.typeName); context.report({ node, message: 'called on {{name}}', data: { name }, - }); + } as any); }, }; }, diff --git a/packages/parser/tests/lib/parser.ts b/packages/parser/tests/lib/parser.ts index c3c205509dd..9545633cd6e 100644 --- a/packages/parser/tests/lib/parser.ts +++ b/packages/parser/tests/lib/parser.ts @@ -1,3 +1,4 @@ +import { TSESLint } from '@typescript-eslint/experimental-utils'; import * as typescriptESTree from '@typescript-eslint/typescript-estree'; import { parse, parseForESLint, Syntax } from '../../src/parser'; import * as scope from '../../src/analyze-scope'; @@ -35,13 +36,13 @@ describe('parser', () => { it('parseAndGenerateServices() should be called with options', () => { const code = 'const valid = true;'; const spy = jest.spyOn(typescriptESTree, 'parseAndGenerateServices'); - const config = { + const config: TSESLint.ParserOptions = { loc: false, comment: false, range: false, tokens: false, sourceType: 'module' as 'module', - ecmaVersion: 10, + ecmaVersion: 2018, ecmaFeatures: { globalReturn: false, jsx: false, diff --git a/packages/parser/tests/lib/tsx.ts b/packages/parser/tests/lib/tsx.ts index eed70b17e8d..21937886b5d 100644 --- a/packages/parser/tests/lib/tsx.ts +++ b/packages/parser/tests/lib/tsx.ts @@ -1,4 +1,4 @@ -import { Linter } from 'eslint'; +import { TSESLint } from '@typescript-eslint/experimental-utils'; import fs from 'fs'; import glob from 'glob'; import * as parser from '../../src/parser'; @@ -31,7 +31,7 @@ describe('TSX', () => { }); describe("if the filename ends with '.tsx', enable jsx option automatically.", () => { - const linter = new Linter(); + const linter = new TSESLint.Linter(); linter.defineParser('@typescript-eslint/parser', parser); it('filePath was not provided', () => { diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index e0e535c774a..f8151e1b268 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -102,7 +102,7 @@ export class Converter { this.allowPattern = allowPattern; } - let result = this.convertNode(node as TSNode, parent || node.parent); + const result = this.convertNode(node as TSNode, parent || node.parent); this.registerTSNodeInNodeMap(node, result); @@ -1390,7 +1390,7 @@ export class Converter { case SyntaxKind.ClassDeclaration: case SyntaxKind.ClassExpression: { const heritageClauses = node.heritageClauses || []; - let classNodeType = + const classNodeType = node.kind === SyntaxKind.ClassDeclaration ? AST_NODE_TYPES.ClassDeclaration : AST_NODE_TYPES.ClassExpression; diff --git a/packages/typescript-estree/src/tsconfig-parser.ts b/packages/typescript-estree/src/tsconfig-parser.ts index 44e1f13b28b..641af07a77a 100644 --- a/packages/typescript-estree/src/tsconfig-parser.ts +++ b/packages/typescript-estree/src/tsconfig-parser.ts @@ -82,7 +82,7 @@ export function calculateProjectParserOptions( watchCallback(filePath, ts.FileWatcherEventKind.Changed); } - for (let rawTsconfigPath of extra.projects) { + for (const rawTsconfigPath of extra.projects) { const tsconfigPath = getTsconfigPath(rawTsconfigPath, extra); const existingWatch = knownWatchProgramMap.get(tsconfigPath); diff --git a/yarn.lock b/yarn.lock index 6541649af58..4f4a0d81f56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1187,19 +1187,6 @@ resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== -"@types/eslint@^4.16.3": - version "4.16.6" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-4.16.6.tgz#96d4ecddbea618ab0b55eaf0dffedf387129b06c" - integrity sha512-GL7tGJig55FeclpOytU7nCCqtR143jBoC7AUdH0DO9xBSIFiNNUFCY/S3KNWsHeQJuU3hjw/OC1+kRTFNXqUZQ== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*": - version "0.0.39" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" - integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== - "@types/events@*": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" @@ -1231,7 +1218,7 @@ dependencies: "@types/jest-diff" "*" -"@types/json-schema@*": +"@types/json-schema@^7.0.3": version "7.0.3" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==