From b5ef704d2620acac5dbfa8bc21a859b898d61286 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 13 Jan 2020 11:13:04 -0800 Subject: [PATCH 01/17] fix(eslint-plugin): set default-param-last as an extension rule (#1445) --- packages/eslint-plugin/src/configs/all.json | 1 + packages/eslint-plugin/tools/generate-configs.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index b8eff6e78b95..3ae4cb1d9620 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -10,6 +10,7 @@ "@typescript-eslint/brace-style": "error", "@typescript-eslint/consistent-type-assertions": "error", "@typescript-eslint/consistent-type-definitions": "error", + "default-param-last": "off", "@typescript-eslint/default-param-last": "error", "@typescript-eslint/explicit-function-return-type": "error", "@typescript-eslint/explicit-member-accessibility": "error", diff --git a/packages/eslint-plugin/tools/generate-configs.ts b/packages/eslint-plugin/tools/generate-configs.ts index 2c7bcfe40cb8..d47d01bfdc0f 100644 --- a/packages/eslint-plugin/tools/generate-configs.ts +++ b/packages/eslint-plugin/tools/generate-configs.ts @@ -24,6 +24,7 @@ const DEFAULT_RULE_SETTING = 'warn'; const BASE_RULES_TO_BE_OVERRIDDEN = new Set([ 'brace-style', 'camelcase', + 'default-param-last', 'func-call-spacing', 'indent', 'no-array-constructor', From 47266053d311e867fb962f2dfaefab684c72a69e Mon Sep 17 00:00:00 2001 From: Jonathan Delgado Date: Mon, 13 Jan 2020 19:45:48 -0800 Subject: [PATCH 02/17] fix(eslint-plugin): [naming-convention] handle empty array-pattern (#1450) --- .../eslint-plugin/src/rules/naming-convention.ts | 4 +++- .../tests/rules/naming-convention.test.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 50acf190d046..aa66caee0f9c 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -688,7 +688,9 @@ function getIdentifiersFromPattern( case AST_NODE_TYPES.ArrayPattern: pattern.elements.forEach(element => { - getIdentifiersFromPattern(element, identifiers); + if (element !== null) { + getIdentifiersFromPattern(element, identifiers); + } }); break; diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index de4625fbeb20..04bd85035543 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -684,6 +684,18 @@ ruleTester.run('naming-convention', rule, { }, ], }, + { + code: ` + const match = 'test'.match(/test/); + const [, key, value] = match; + `, + options: [ + { + selector: 'default', + format: ['camelCase'], + }, + ], + }, ], invalid: [ ...createInvalidTestCases(cases), From 8dcdfb134dac07dec9a56b064abe0a67ba5a8592 Mon Sep 17 00:00:00 2001 From: Armano Date: Tue, 14 Jan 2020 05:19:49 +0100 Subject: [PATCH 03/17] test(experimental-utils): add few missing tests (#1449) --- .../tests/eslint-utils/RuleCreator.test.ts | 46 +++++++++++ .../tests/eslint-utils/applyDefault.test.ts | 18 ++++- .../batchedSingleLineTests.test.ts | 81 +++++++++++++++++++ .../tests/eslint-utils/deepMerge.test.ts | 8 +- 4 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 packages/experimental-utils/tests/eslint-utils/RuleCreator.test.ts create mode 100644 packages/experimental-utils/tests/eslint-utils/batchedSingleLineTests.test.ts diff --git a/packages/experimental-utils/tests/eslint-utils/RuleCreator.test.ts b/packages/experimental-utils/tests/eslint-utils/RuleCreator.test.ts new file mode 100644 index 000000000000..343d5f0e1aad --- /dev/null +++ b/packages/experimental-utils/tests/eslint-utils/RuleCreator.test.ts @@ -0,0 +1,46 @@ +import { ESLintUtils } from '../../src'; + +describe('RuleCreator', () => { + const createRule = ESLintUtils.RuleCreator(name => `test/${name}`); + + it('createRule should be a function', () => { + expect(typeof createRule).toEqual('function'); + }); + + it('should create rule correctly', () => { + const rule = createRule({ + name: 'test', + meta: { + docs: { + description: 'some description', + category: 'Best Practices', + recommended: 'error', + requiresTypeChecking: true, + }, + messages: { + foo: 'some message', + }, + schema: [], + type: 'problem', + }, + defaultOptions: [], + create() { + return {}; + }, + }); + expect(rule.meta).toEqual({ + docs: { + description: 'some description', + category: 'Best Practices', + url: 'test/test', + recommended: 'error', + requiresTypeChecking: true, + }, + messages: { + foo: 'some message', + }, + schema: [], + type: 'problem', + }); + }); +}); diff --git a/packages/experimental-utils/tests/eslint-utils/applyDefault.test.ts b/packages/experimental-utils/tests/eslint-utils/applyDefault.test.ts index c3304ec48714..ec9411c31c25 100644 --- a/packages/experimental-utils/tests/eslint-utils/applyDefault.test.ts +++ b/packages/experimental-utils/tests/eslint-utils/applyDefault.test.ts @@ -1,10 +1,10 @@ -import * as util from '../../src/eslint-utils/applyDefault'; +import { ESLintUtils } from '../../src'; describe('applyDefault', () => { it('returns a clone of the default if no options given', () => { const defaults = [{ prop: 'setting' }]; const user = null; - const result = util.applyDefault(defaults, user); + const result = ESLintUtils.applyDefault(defaults, user); expect(result).toStrictEqual(defaults); expect(result).not.toBe(defaults); @@ -26,7 +26,7 @@ describe('applyDefault', () => { other: 'something', }, ]; - const result = util.applyDefault(defaults, user); + const result = ESLintUtils.applyDefault(defaults, user); expect(result).toStrictEqual([ { @@ -44,9 +44,19 @@ describe('applyDefault', () => { it('returns a brand new array', () => { const defaults: undefined[] = []; const user: undefined[] = []; - const result = util.applyDefault(defaults, user); + const result = ESLintUtils.applyDefault(defaults, user); expect(result).not.toBe(defaults); expect(result).not.toBe(user); }); + + it('should work with array of options', () => { + const defaults: unknown[] = ['1tbs']; + const user: unknown[] = ['2tbs']; + const result = ESLintUtils.applyDefault(defaults, user); + + expect(result).toStrictEqual(['2tbs']); + expect(result).not.toBe(defaults); + expect(result).not.toBe(user); + }); }); diff --git a/packages/experimental-utils/tests/eslint-utils/batchedSingleLineTests.test.ts b/packages/experimental-utils/tests/eslint-utils/batchedSingleLineTests.test.ts new file mode 100644 index 000000000000..596bb480b945 --- /dev/null +++ b/packages/experimental-utils/tests/eslint-utils/batchedSingleLineTests.test.ts @@ -0,0 +1,81 @@ +import { ESLintUtils } from '../../src'; + +describe('batchedSingleLineTests', () => { + const FIXTURES = ` +a +b +c + `; + const errors = [ + { messageId: 'someMessage1', line: 2 }, + { messageId: 'someMessage2', line: 3 }, + { messageId: 'someMessage3', line: 4 }, + ]; + const options = [{ optionOne: 'value' }]; + + it('should work without options', () => { + expect( + ESLintUtils.batchedSingleLineTests({ + code: FIXTURES, + }), + ).toEqual([ + { code: 'a', errors: [] }, + { code: 'b', errors: [] }, + { code: 'c', errors: [] }, + ]); + }); + + it('should work with errors', () => { + expect( + ESLintUtils.batchedSingleLineTests({ + code: FIXTURES, + errors, + }), + ).toEqual([ + { code: 'a', errors: [{ messageId: 'someMessage1', line: 1 }] }, + { code: 'b', errors: [{ messageId: 'someMessage2', line: 1 }] }, + { code: 'c', errors: [{ messageId: 'someMessage3', line: 1 }] }, + ]); + }); + + it('should work with all fields', () => { + const filename = 'foo.ts'; + const parser = 'some-parser'; + + expect( + ESLintUtils.batchedSingleLineTests({ + code: FIXTURES, + errors, + options, + parser, + output: FIXTURES, + filename, + }), + ).toEqual([ + { + code: 'a', + output: 'a', + errors: [{ messageId: 'someMessage1', line: 1 }], + parser, + filename, + options, + }, + { + code: 'b', + output: 'b', + errors: [{ messageId: 'someMessage2', line: 1 }], + parser, + filename, + options, + }, + { + code: 'c', + output: 'c', + errors: [{ messageId: 'someMessage3', line: 1 }], + parser, + filename, + options, + }, + ]); + }); +}); diff --git a/packages/experimental-utils/tests/eslint-utils/deepMerge.test.ts b/packages/experimental-utils/tests/eslint-utils/deepMerge.test.ts index 7caee854d492..26fb02e3bc4d 100644 --- a/packages/experimental-utils/tests/eslint-utils/deepMerge.test.ts +++ b/packages/experimental-utils/tests/eslint-utils/deepMerge.test.ts @@ -1,10 +1,10 @@ -import * as util from '../../src/eslint-utils/deepMerge'; +import { ESLintUtils } from '../../src'; describe('deepMerge', () => { it('creates a brand new object', () => { const a = {}; const b = {}; - const result = util.deepMerge(a, b); + const result = ESLintUtils.deepMerge(a, b); expect(result).not.toBe(a); expect(result).not.toBe(b); @@ -38,7 +38,7 @@ describe('deepMerge', () => { }, }; - expect(util.deepMerge(a, b)).toStrictEqual(Object.assign({}, a, b)); + expect(ESLintUtils.deepMerge(a, b)).toStrictEqual(Object.assign({}, a, b)); }); it('deeply overwrites properties in the first one with the second', () => { @@ -53,6 +53,6 @@ describe('deepMerge', () => { }, }; - expect(util.deepMerge(a, b)).toStrictEqual(b); + expect(ESLintUtils.deepMerge(a, b)).toStrictEqual(b); }); }); From 62e4ca0b718cf781462a00d0514023d7c6874ebd Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 13 Jan 2020 20:20:02 -0800 Subject: [PATCH 04/17] fix(typescript-estree): correct type of `ArrayPattern.elements` (#1451) --- .../src/rules/indent-new-do-not-use/index.ts | 10 ++++++---- packages/typescript-estree/src/ts-estree/ts-estree.ts | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/indent-new-do-not-use/index.ts b/packages/eslint-plugin/src/rules/indent-new-do-not-use/index.ts index d8c3775d1c92..6427439e7b81 100644 --- a/packages/eslint-plugin/src/rules/indent-new-do-not-use/index.ts +++ b/packages/eslint-plugin/src/rules/indent-new-do-not-use/index.ts @@ -578,7 +578,7 @@ export default createRule({ * @param offset The amount that the elements should be offset */ function addElementListIndent( - elements: TSESTree.Node[], + elements: (TSESTree.Node | null)[], startToken: TSESTree.Token, endToken: TSESTree.Token, offset: number | string, @@ -606,7 +606,8 @@ export default createRule({ offsets.setDesiredOffset(endToken, startToken, 0); // If the preference is "first" but there is no first element (e.g. sparse arrays w/ empty first slot), fall back to 1 level. - if (offset === 'first' && elements.length && !elements[0]) { + const firstElement = elements[0]; + if (offset === 'first' && elements.length && !firstElement) { return; } elements.forEach((element, index) => { @@ -628,7 +629,7 @@ export default createRule({ tokenInfo.isFirstTokenOfLine(getFirstToken(element)) ) { offsets.matchOffsetOf( - getFirstToken(elements[0]), + getFirstToken(firstElement!), getFirstToken(element), ); } else { @@ -640,6 +641,7 @@ export default createRule({ if ( previousElement && + previousElementLastToken && previousElementLastToken.loc.end.line - countTrailingLinebreaks(previousElementLastToken.value) > startToken.loc.end.line @@ -854,7 +856,7 @@ export default createRule({ ) { const openingBracket = sourceCode.getFirstToken(node)!; const closingBracket = sourceCode.getTokenAfter( - node.elements[node.elements.length - 1] || openingBracket, + node.elements[node.elements.length - 1] ?? openingBracket, isClosingBracketToken, )!; diff --git a/packages/typescript-estree/src/ts-estree/ts-estree.ts b/packages/typescript-estree/src/ts-estree/ts-estree.ts index db52a442861b..d0aa4b17c6ce 100644 --- a/packages/typescript-estree/src/ts-estree/ts-estree.ts +++ b/packages/typescript-estree/src/ts-estree/ts-estree.ts @@ -681,7 +681,7 @@ export interface ArrayExpression extends BaseNode { export interface ArrayPattern extends BaseNode { type: AST_NODE_TYPES.ArrayPattern; - elements: DestructuringPattern[]; + elements: (DestructuringPattern | null)[]; typeAnnotation?: TSTypeAnnotation; optional?: boolean; decorators?: Decorator[]; From b529a4c603dc7f2a327c2364a47f34c77aab54ac Mon Sep 17 00:00:00 2001 From: Dieter Oberkofler Date: Tue, 14 Jan 2020 15:39:52 +0100 Subject: [PATCH 05/17] docs(eslint-plugin): fix misspelled `naming-convention` rule name --- .../eslint-plugin/docs/rules/naming-convention.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index 5186c98eb6db..d49d8f26a2e6 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -229,7 +229,7 @@ Group Selectors are provided for convenience, and essentially bundle up sets of ```json { - "@typescript-eslint/naming-conventions": [ + "@typescript-eslint/naming-convention": [ "error", { "selector": "variableLike", "format": ["camelCase"] } ] @@ -240,7 +240,7 @@ Group Selectors are provided for convenience, and essentially bundle up sets of ```json { - "@typescript-eslint/naming-conventions": [ + "@typescript-eslint/naming-convention": [ "error", { "selector": "memberLike", @@ -256,7 +256,7 @@ Group Selectors are provided for convenience, and essentially bundle up sets of ```json { - "@typescript-eslint/naming-conventions": [ + "@typescript-eslint/naming-convention": [ "error", { "selector": "variable", @@ -272,7 +272,7 @@ Group Selectors are provided for convenience, and essentially bundle up sets of ```json { - "@typescript-eslint/naming-conventions": [ + "@typescript-eslint/naming-convention": [ "error", { "selector": "variable", @@ -286,7 +286,7 @@ Group Selectors are provided for convenience, and essentially bundle up sets of ```json { - "@typescript-eslint/naming-conventions": [ + "@typescript-eslint/naming-convention": [ "error", { "selector": "typeParameter", @@ -301,7 +301,7 @@ Group Selectors are provided for convenience, and essentially bundle up sets of ```json { - "@typescript-eslint/naming-conventions": [ + "@typescript-eslint/naming-convention": [ "error", { "selector": "default", From 982c8bc37958dfbf7bfb6e74f270866ae322995e Mon Sep 17 00:00:00 2001 From: Armano Date: Tue, 14 Jan 2020 18:05:47 +0100 Subject: [PATCH 06/17] feat(experimental-utils): expose getParserServices from utils (#1448) * feat(experimental-utils): expose getParserServices from utils * test: add experimental-utils to integration tests --- .../eslint-plugin-tslint/src/rules/config.ts | 17 ++--------------- .../eslint-plugin-tslint/tests/index.spec.ts | 2 +- packages/eslint-plugin/src/util/index.ts | 10 +++++++--- .../src/eslint-utils}/getParserServices.ts | 12 +++++------- .../src/eslint-utils/index.ts | 1 + tests/integration/docker-compose.yml | 10 ++++++++++ tests/integration/fixtures/markdown/test.sh | 1 + .../test.sh | 1 + .../test.sh | 1 + tests/integration/fixtures/vue-jsx/test.sh | 1 + tests/integration/fixtures/vue-sfc/test.sh | 1 + 11 files changed, 31 insertions(+), 26 deletions(-) rename packages/{eslint-plugin/src/util => experimental-utils/src/eslint-utils}/getParserServices.ts (69%) diff --git a/packages/eslint-plugin-tslint/src/rules/config.ts b/packages/eslint-plugin-tslint/src/rules/config.ts index 05608387e79a..fcfaa7e2b8fd 100644 --- a/packages/eslint-plugin-tslint/src/rules/config.ts +++ b/packages/eslint-plugin-tslint/src/rules/config.ts @@ -1,7 +1,4 @@ -import { - ESLintUtils, - ParserServices, -} from '@typescript-eslint/experimental-utils'; +import { ESLintUtils } from '@typescript-eslint/experimental-utils'; import memoize from 'lodash/memoize'; import { Configuration, RuleSeverity } from 'tslint'; import { CustomLinter } from '../custom-linter'; @@ -101,17 +98,7 @@ export default createRule({ 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`, - ); - } + const parserServices = ESLintUtils.getParserServices(context); /** * The TSLint rules configuration passed in by the user diff --git a/packages/eslint-plugin-tslint/tests/index.spec.ts b/packages/eslint-plugin-tslint/tests/index.spec.ts index c69ddbeedd08..5241f8907caa 100644 --- a/packages/eslint-plugin-tslint/tests/index.spec.ts +++ b/packages/eslint-plugin-tslint/tests/index.spec.ts @@ -155,7 +155,7 @@ describe('tslint/error', () => { linter.defineParser('@typescript-eslint/parser', parser); expect(() => linter.verify(code, config)).toThrow( - `You must provide a value for the "parserOptions.project" property for @typescript-eslint/parser`, + 'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.', ); } diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index cb0430c114d7..7fde0f41e715 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -2,11 +2,15 @@ import { ESLintUtils } from '@typescript-eslint/experimental-utils'; export * from './astUtils'; export * from './createRule'; -export * from './getParserServices'; export * from './misc'; export * from './nullThrows'; export * from './types'; // this is done for convenience - saves migrating all of the old rules -const { applyDefault, deepMerge, isObjectNotArray } = ESLintUtils; -export { applyDefault, deepMerge, isObjectNotArray }; +const { + applyDefault, + deepMerge, + isObjectNotArray, + getParserServices, +} = ESLintUtils; +export { applyDefault, deepMerge, isObjectNotArray, getParserServices }; diff --git a/packages/eslint-plugin/src/util/getParserServices.ts b/packages/experimental-utils/src/eslint-utils/getParserServices.ts similarity index 69% rename from packages/eslint-plugin/src/util/getParserServices.ts rename to packages/experimental-utils/src/eslint-utils/getParserServices.ts index d0a184871718..65607e77290f 100644 --- a/packages/eslint-plugin/src/util/getParserServices.ts +++ b/packages/experimental-utils/src/eslint-utils/getParserServices.ts @@ -1,7 +1,7 @@ -import { - ParserServices, - TSESLint, -} from '@typescript-eslint/experimental-utils'; +import { ParserServices, TSESLint } from '../'; + +const ERROR_MESSAGE = + 'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.'; type RequiredParserServices = { [k in keyof ParserServices]: Exclude; @@ -25,9 +25,7 @@ export function getParserServices< * The user needs to have configured "project" in their parserOptions * for @typescript-eslint/parser */ - throw new Error( - 'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.', - ); + throw new Error(ERROR_MESSAGE); } return context.parserServices as RequiredParserServices; } diff --git a/packages/experimental-utils/src/eslint-utils/index.ts b/packages/experimental-utils/src/eslint-utils/index.ts index c8069ca6fc29..72977de2003f 100644 --- a/packages/experimental-utils/src/eslint-utils/index.ts +++ b/packages/experimental-utils/src/eslint-utils/index.ts @@ -1,4 +1,5 @@ export * from './applyDefault'; export * from './batchedSingleLineTests'; +export * from './getParserServices'; export * from './RuleCreator'; export * from './deepMerge'; diff --git a/tests/integration/docker-compose.yml b/tests/integration/docker-compose.yml index c3a0b450d65f..a92dff5e0903 100644 --- a/tests/integration/docker-compose.yml +++ b/tests/integration/docker-compose.yml @@ -13,6 +13,8 @@ services: - /usr/parser/tests - ../../packages/typescript-estree/:/usr/typescript-estree - /usr/typescript-estree/tests + - ../../packages/experimental-utils/:/usr/experimental-utils + - /usr/experimental-utils/tests - ../../packages/eslint-plugin/:/usr/eslint-plugin - /usr/eslint-plugin/tests - ../../packages/eslint-plugin-tslint/:/usr/eslint-plugin-tslint @@ -32,6 +34,8 @@ services: - /usr/parser/tests - ../../packages/typescript-estree/:/usr/typescript-estree - /usr/typescript-estree/tests + - ../../packages/experimental-utils/:/usr/experimental-utils + - /usr/experimental-utils/tests - ../../packages/eslint-plugin/:/usr/eslint-plugin - /usr/eslint-plugin/tests # Runtime link to all the specific integration test files, so that most updates don't require a rebuild. @@ -49,6 +53,8 @@ services: - /usr/parser/tests - ../../packages/typescript-estree/:/usr/typescript-estree - /usr/typescript-estree/tests + - ../../packages/experimental-utils/:/usr/experimental-utils + - /usr/experimental-utils/tests - ../../packages/eslint-plugin/:/usr/eslint-plugin - /usr/eslint-plugin/tests # Runtime link to all the specific integration test files, so that most updates don't require a rebuild. @@ -66,6 +72,8 @@ services: - /usr/parser/tests - ../../packages/typescript-estree/:/usr/typescript-estree - /usr/typescript-estree/tests + - ../../packages/experimental-utils/:/usr/experimental-utils + - /usr/experimental-utils/tests - ../../packages/eslint-plugin/:/usr/eslint-plugin - /usr/eslint-plugin/tests # Runtime link to all the specific integration test files, so that most updates don't require a rebuild. @@ -83,6 +91,8 @@ services: - /usr/parser/tests - ../../packages/typescript-estree/:/usr/typescript-estree - /usr/typescript-estree/tests + - ../../packages/experimental-utils/:/usr/experimental-utils + - /usr/experimental-utils/tests - ../../packages/eslint-plugin/:/usr/eslint-plugin - /usr/eslint-plugin/tests # Runtime link to all the specific integration test files, so that most updates don't require a rebuild. diff --git a/tests/integration/fixtures/markdown/test.sh b/tests/integration/fixtures/markdown/test.sh index 4641a6b33f45..6856a3f7c1b8 100755 --- a/tests/integration/fixtures/markdown/test.sh +++ b/tests/integration/fixtures/markdown/test.sh @@ -9,6 +9,7 @@ npm install # Use the local volumes for our own packages npm install $(npm pack /usr/typescript-estree | tail -1) npm install $(npm pack /usr/parser | tail -1) +npm install $(npm pack /usr/experimental-utils | tail -1) npm install $(npm pack /usr/eslint-plugin | tail -1) # Install the latest vue-eslint-parser (this may break us occassionally, but it's probably good to get that feedback early) diff --git a/tests/integration/fixtures/recommended-does-not-require-program/test.sh b/tests/integration/fixtures/recommended-does-not-require-program/test.sh index 1fa77f5cbdf7..4cf1ad5505e6 100755 --- a/tests/integration/fixtures/recommended-does-not-require-program/test.sh +++ b/tests/integration/fixtures/recommended-does-not-require-program/test.sh @@ -9,6 +9,7 @@ npm install # Use the local volumes for our own packages npm install $(npm pack /usr/typescript-estree | tail -1) npm install $(npm pack /usr/parser | tail -1) +npm install $(npm pack /usr/experimental-utils | tail -1) npm install $(npm pack /usr/eslint-plugin | tail -1) # Run the linting diff --git a/tests/integration/fixtures/typescript-and-tslint-plugins-together/test.sh b/tests/integration/fixtures/typescript-and-tslint-plugins-together/test.sh index dbd94edf768b..4746867a40ae 100755 --- a/tests/integration/fixtures/typescript-and-tslint-plugins-together/test.sh +++ b/tests/integration/fixtures/typescript-and-tslint-plugins-together/test.sh @@ -9,6 +9,7 @@ npm install # Use the local volumes for our own packages npm install $(npm pack /usr/typescript-estree | tail -1) npm install $(npm pack /usr/parser | tail -1) +npm install $(npm pack /usr/experimental-utils | tail -1) npm install $(npm pack /usr/eslint-plugin-tslint | tail -1) npm install $(npm pack /usr/eslint-plugin | tail -1) diff --git a/tests/integration/fixtures/vue-jsx/test.sh b/tests/integration/fixtures/vue-jsx/test.sh index f59b6348fd6b..30ec8940d3c0 100755 --- a/tests/integration/fixtures/vue-jsx/test.sh +++ b/tests/integration/fixtures/vue-jsx/test.sh @@ -9,6 +9,7 @@ npm install # Use the local volumes for our own packages npm install $(npm pack /usr/typescript-estree | tail -1) npm install $(npm pack /usr/parser | tail -1) +npm install $(npm pack /usr/experimental-utils | tail -1) npm install $(npm pack /usr/eslint-plugin | tail -1) # Install the latest vue-eslint-parser (this may break us occassionally, but it's probably good to get that feedback early) diff --git a/tests/integration/fixtures/vue-sfc/test.sh b/tests/integration/fixtures/vue-sfc/test.sh index f59b6348fd6b..30ec8940d3c0 100755 --- a/tests/integration/fixtures/vue-sfc/test.sh +++ b/tests/integration/fixtures/vue-sfc/test.sh @@ -9,6 +9,7 @@ npm install # Use the local volumes for our own packages npm install $(npm pack /usr/typescript-estree | tail -1) npm install $(npm pack /usr/parser | tail -1) +npm install $(npm pack /usr/experimental-utils | tail -1) npm install $(npm pack /usr/eslint-plugin | tail -1) # Install the latest vue-eslint-parser (this may break us occassionally, but it's probably good to get that feedback early) From b47da0de38420ba2337601bc2127928259dcf315 Mon Sep 17 00:00:00 2001 From: Armano Date: Tue, 14 Jan 2020 20:30:38 +0100 Subject: [PATCH 07/17] docs(typescript-estree): clarify filePath resolution in docs (#1458) --- packages/typescript-estree/README.md | 4 ++-- packages/typescript-estree/src/parser-options.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/typescript-estree/README.md b/packages/typescript-estree/README.md index 1d9a38d0fb34..54706cf47f9e 100644 --- a/packages/typescript-estree/README.md +++ b/packages/typescript-estree/README.md @@ -61,7 +61,7 @@ interface ParseOptions { errorOnUnknownASTType?: boolean; /** - * The absolute path to the file being parsed. + * Absolute (or relative to `cwd`) path to the file being parsed. */ filePath?: string; @@ -159,7 +159,7 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { extraFileExtensions?: string[]; /** - * The absolute path to the file being parsed. + * Absolute (or relative to `tsconfigRootDir`) path to the file being parsed. * When `project` is provided, this is required, as it is used to fetch the file from the TypeScript compiler's cache. */ filePath?: string; diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 1cd8b4d54750..2317a683abfe 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -65,7 +65,7 @@ export interface TSESTreeOptions { extraFileExtensions?: string[]; /** - * The absolute path to the file being parsed. + * Absolute (or relative to `tsconfigRootDir`) path to the file being parsed. * When `project` is provided, this is required, as it is used to fetch the file from the TypeScript compiler's cache. */ filePath?: string; From 61eb43473a108c3ce067982f1bb1ff5af3e254aa Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Tue, 14 Jan 2020 11:58:02 -0800 Subject: [PATCH 08/17] feat(eslint-plugin): [naming-convention] allow not check format (#1455) --- .../docs/rules/naming-convention.md | 24 ++++++++----- .../src/rules/naming-convention.ts | 34 +++++++++++-------- .../tests/rules/naming-convention.test.ts | 28 +++++++++++++++ 3 files changed, 62 insertions(+), 24 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index d49d8f26a2e6..5ede1186e99b 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -19,14 +19,16 @@ Each property will be described in detail below. Also see the examples section b ```ts type Options = { // format options - format: ( - | 'camelCase' - | 'strictCamelCase' - | 'PascalCase' - | 'StrictPascalCase' - | 'snake_case' - | 'UPPER_CASE' - )[]; + format: + | ( + | 'camelCase' + | 'strictCamelCase' + | 'PascalCase' + | 'StrictPascalCase' + | 'snake_case' + | 'UPPER_CASE' + )[] + | null; custom?: { regex: string; match: boolean; @@ -79,7 +81,7 @@ When the format of an identifier is checked, it is checked in the following orde 1. validate custom 1. validate format -At each step, if the identifier matches the option, the matching part will be removed. +For steps 1-4, if the identifier matches the option, the matching part will be removed. For example, if you provide the following formatting option: `{ leadingUnderscore: 'allow', prefix: ['I'], format: ['StrictPascalCase'] }`, for the identifier `_IMyInterface`, then the following checks will occur: 1. `name = _IMyInterface` @@ -89,6 +91,7 @@ For example, if you provide the following formatting option: `{ leadingUnderscor 1. validate prefix - pass - Trim prefix - `name = MyInterface` 1. validate suffix - no check +1. validate custom - no check 1. validate format - pass One final note is that if the name were to become empty via this trimming process, it is considered to match all `format`s. An example of where this might be useful is for generic type parameters, where you want all names to be prefixed with `T`, but also want to allow for the single character `T` name. @@ -104,6 +107,9 @@ The `format` option defines the allowed formats for the identifier. This option - `snake_case` - standard snake_case format - all characters must be lower-case, and underscores are allowed. - `UPPER_CASE` - same as `snake_case`, except all characters must be upper-case. +Instead of an array, you may also pass `null`. This signifies "this selector shall not have its format checked". +This can be useful if you want to enforce no particular format for a specific selector, after applying a group selector. + ### `custom` The `custom` option defines a custom regex that the identifier must (or must not) match. This option allows you to have a bit more finer-grained control over identifiers, letting you ban (or force) certain patterns and substrings. diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index aa66caee0f9c..8f3fe91ed5d3 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -100,7 +100,7 @@ type TypeModifiersString = keyof typeof TypeModifiers; interface Selector { // format options - format: PredefinedFormatsString[]; + format: PredefinedFormatsString[] | null; custom?: { regex: string; match: boolean; @@ -117,7 +117,7 @@ interface Selector { } interface NormalizedSelector { // format options - format: PredefinedFormats[]; + format: PredefinedFormats[] | null; custom: { regex: RegExp; match: boolean; @@ -154,19 +154,24 @@ const PREFIX_SUFFIX_SCHEMA: JSONSchema.JSONSchema4 = { type: 'string', minLength: 1, }, - minItems: 1, additionalItems: false, }; type JSONSchemaProperties = Record; const FORMAT_OPTIONS_PROPERTIES: JSONSchemaProperties = { format: { - type: 'array', - items: { - type: 'string', - enum: util.getEnumNames(PredefinedFormats), - }, - minItems: 1, - additionalItems: false, + oneOf: [ + { + type: 'array', + items: { + type: 'string', + enum: util.getEnumNames(PredefinedFormats), + }, + additionalItems: false, + }, + { + type: 'null', + }, + ], }, custom: { type: 'object', @@ -235,7 +240,6 @@ function selectorSchema( } const SCHEMA: JSONSchema.JSONSchema4 = { type: 'array', - minItems: 1, items: { oneOf: [ ...selectorSchema('default', false, util.getEnumNames(Modifiers)), @@ -1024,7 +1028,7 @@ function createValidator( originalName: string, ): boolean { const formats = config.format; - if (formats.length === 0) { + if (formats === null || formats.length === 0) { return true; } @@ -1189,7 +1193,7 @@ function normalizeOption(option: Selector): NormalizedSelector { return { // format options - format: option.format.map(f => PredefinedFormats[f]), + format: option.format ? option.format.map(f => PredefinedFormats[f]) : null, custom: option.custom ? { regex: new RegExp(option.custom.regex), @@ -1204,8 +1208,8 @@ function normalizeOption(option: Selector): NormalizedSelector { option.trailingUnderscore !== undefined ? UnderscoreOptions[option.trailingUnderscore] : null, - prefix: option.prefix ?? null, - suffix: option.suffix ?? null, + prefix: option.prefix && option.prefix.length > 0 ? option.prefix : null, + suffix: option.suffix && option.suffix.length > 0 ? option.suffix : null, // selector options selector: isMetaSelector(option.selector) ? MetaSelectors[option.selector] diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index 04bd85035543..04d83717522a 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -603,6 +603,7 @@ const cases: Cases = [ ruleTester.run('naming-convention', rule, { valid: [ + `const x = 1;`, // no options shouldn't crash ...createValidTestCases(cases), { code: ` @@ -696,6 +697,33 @@ ruleTester.run('naming-convention', rule, { }, ], }, + // no format selector + { + code: 'const snake_case = 1;', + options: [ + { + selector: 'default', + format: ['camelCase'], + }, + { + selector: 'variable', + format: null, + }, + ], + }, + { + code: 'const snake_case = 1;', + options: [ + { + selector: 'default', + format: ['camelCase'], + }, + { + selector: 'variable', + format: [], + }, + ], + }, ], invalid: [ ...createInvalidTestCases(cases), From bb0a84647c1021f3731344582a60b5bd3c679478 Mon Sep 17 00:00:00 2001 From: GuyPie Date: Wed, 15 Jan 2020 21:16:11 +0100 Subject: [PATCH 09/17] feat(eslint-plugin): add explicit-module-boundary-types rule (#1020) Co-authored-by: Brad Zacher --- packages/eslint-plugin/README.md | 2 +- .../rules/explicit-module-boundary-types.md | 208 +++++ packages/eslint-plugin/src/configs/all.json | 2 +- .../rules/explicit-module-boundary-types.ts | 464 ++++++++++ packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/no-untyped-public-signature.ts | 2 + .../explicit-module-boundary-types.test.ts | 802 ++++++++++++++++++ 7 files changed, 1480 insertions(+), 2 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md create mode 100644 packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts create mode 100644 packages/eslint-plugin/tests/rules/explicit-module-boundary-types.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 2e4c9c5ae46f..0f81cf64216e 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -106,6 +106,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | | | [`@typescript-eslint/explicit-function-return-type`](./docs/rules/explicit-function-return-type.md) | Require explicit return types on functions and class methods | :heavy_check_mark: | | | | [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | | | +| [`@typescript-eslint/explicit-module-boundary-types`](./docs/rules/explicit-module-boundary-types.md) | Require explicit return and argument types on exported functions' and classes' public class methods | | | | | [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | | | [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | | | [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | :heavy_check_mark: | :wrench: | | @@ -138,7 +139,6 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/no-unnecessary-qualifier`](./docs/rules/no-unnecessary-qualifier.md) | Warns when a namespace qualifier is unnecessary | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-type-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Enforces that type arguments will not be used if not required | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | :heavy_check_mark: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-untyped-public-signature`](./docs/rules/no-untyped-public-signature.md) | Disallow untyped public methods | | | | | [`@typescript-eslint/no-unused-expressions`](./docs/rules/no-unused-expressions.md) | Disallow unused expressions | | | | | [`@typescript-eslint/no-unused-vars`](./docs/rules/no-unused-vars.md) | Disallow unused variables | :heavy_check_mark: | | | | [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments | | | :thought_balloon: | diff --git a/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md b/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md new file mode 100644 index 000000000000..cc7d5f1cc03c --- /dev/null +++ b/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md @@ -0,0 +1,208 @@ +# Require explicit return and argument types on exported functions' and classes' public class methods (`explicit-module-boundary-types`) + +Explicit types for function return values and arguments makes it clear to any calling code what is the module boundary's input and output. + +Consider using this rule in place of [`no-untyped-public-signature`](./no-untyped-public-signature.md) which has been deprecated. + +## Rule Details + +This rule aims to ensure that the values returned from a module are of the expected type. + +The following patterns are considered warnings: + +```ts +// Should indicate that no value is returned (void) +export function test() { + return; +} + +// Should indicate that a number is returned +export default function() { + return 1; +} + +// Should indicate that a string is returned +export var arrowFn = () => 'test'; + +// All arguments should be typed +export var arrowFn = (arg): string => `test ${arg}`; + +export class Test { + // Should indicate that no value is returned (void) + method() { + return; + } +} +``` + +The following patterns are not warnings: + +```ts +// Function is not exported +function test() { + return; +} + +// A return value of type number +export var fn = function(): number { + return 1; +}; + +// A return value of type string +export var arrowFn = (arg: string): string => `test ${arg}`; + +// Class is not exported +class Test { + method() { + return; + } +} +``` + +## Options + +The rule accepts an options object with the following properties: + +```ts +type Options = { + // if true, type annotations are also allowed on the variable of a function expression rather than on the function directly + allowTypedFunctionExpressions?: boolean; + // if true, functions immediately returning another function expression will not be checked + allowHigherOrderFunctions?: boolean; + // if true, body-less arrow functions are allowed to return an object as const + allowDirectConstAssertionInArrowFunctions?: boolean; + // an array of function/method names that will not be checked + allowedNames?: string[]; +}; + +const defaults = { + allowTypedFunctionExpressions: true, + allowHigherOrderFunctions: true, + allowedNames: [], +}; +``` + +### Configuring in a mixed JS/TS codebase + +If you are working on a codebase within which you lint non-TypeScript code (i.e. `.js`/`.jsx`), you should ensure that you should use [ESLint `overrides`](https://eslint.org/docs/user-guide/configuring#disabling-rules-only-for-a-group-of-files) to only enable the rule on `.ts`/`.tsx` files. If you don't, then you will get unfixable lint errors reported within `.js`/`.jsx` files. + +```jsonc +{ + "rules": { + // disable the rule for all files + "@typescript-eslint/explicit-module-boundary-types": "off" + }, + "overrides": [ + { + // enable the rule specifically for TypeScript files + "files": ["*.ts", "*.tsx"], + "rules": { + "@typescript-eslint/explicit-module-boundary-types": ["error"] + } + } + ] +} +``` + +### `allowTypedFunctionExpressions` + +Examples of **incorrect** code for this rule with `{ allowTypedFunctionExpressions: true }`: + +```ts +export let arrowFn = () => 'test'; + +export let funcExpr = function() { + return 'test'; +}; + +export let objectProp = { + foo: () => 1, +}; +``` + +Examples of additional **correct** code for this rule with `{ allowTypedFunctionExpressions: true }`: + +```ts +type FuncType = () => string; + +export let arrowFn: FuncType = () => 'test'; + +export let funcExpr: FuncType = function() { + return 'test'; +}; + +export let asTyped = (() => '') as () => string; +export let castTyped = <() => string>(() => ''); + +interface ObjectType { + foo(): number; +} +export let objectProp: ObjectType = { + foo: () => 1, +}; +export let objectPropAs = { + foo: () => 1, +} as ObjectType; +export let objectPropCast = { + foo: () => 1, +}; +``` + +### `allowHigherOrderFunctions` + +Examples of **incorrect** code for this rule with `{ allowHigherOrderFunctions: true }`: + +```ts +export var arrowFn = () => () => {}; + +export function fn() { + return function() {}; +} +``` + +Examples of **correct** code for this rule with `{ allowHigherOrderFunctions: true }`: + +```ts +export var arrowFn = () => (): void => {}; + +export function fn() { + return function(): void {}; +} +``` + +### `allowDirectConstAssertionInArrowFunctions` + +Examples of additional **correct** code for this rule with `{ allowDirectConstAssertionInArrowFunctions: true }`: + +```ts +export const func = (value: number) => ({ type: 'X', value } as const); +``` + +Examples of additional **incorrect** code for this rule with `{ allowDirectConstAssertionInArrowFunctions: true }`: + +```ts +export const func = (value: number) => ({ type: 'X', value }); +export const foo = () => { + return { + bar: true, + } as const; +}; +``` + +### `allowedNames` + +You may pass function/method names you would like this rule to ignore, like so: + +```cjson +{ + "@typescript-eslint/explicit-module-boundary-types": ["error", { "allowedName": ["ignoredFunctionName", "ignoredMethodName"] }] +} +``` + +## When Not To Use It + +If you wish to make sure all functions have explicit return types, as opposed to only the module boundaries, you can use [explicit-function-return-type](https://github.com/eslint/eslint/blob/master/docs/rules/explicit-function-return-type.md) + +## Further Reading + +- TypeScript [Functions](https://www.typescriptlang.org/docs/handbook/functions.html#function-types) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 3ae4cb1d9620..43dbe291f879 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -14,6 +14,7 @@ "@typescript-eslint/default-param-last": "error", "@typescript-eslint/explicit-function-return-type": "error", "@typescript-eslint/explicit-member-accessibility": "error", + "@typescript-eslint/explicit-module-boundary-types": "error", "func-call-spacing": "off", "@typescript-eslint/func-call-spacing": "error", "indent": "off", @@ -53,7 +54,6 @@ "@typescript-eslint/no-unnecessary-qualifier": "error", "@typescript-eslint/no-unnecessary-type-arguments": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error", - "@typescript-eslint/no-untyped-public-signature": "error", "no-unused-expressions": "off", "@typescript-eslint/no-unused-expressions": "error", "no-unused-vars": "off", diff --git a/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts b/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts new file mode 100644 index 000000000000..eeb9d6e39ae8 --- /dev/null +++ b/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts @@ -0,0 +1,464 @@ +import { + TSESTree, + AST_NODE_TYPES, + AST_TOKEN_TYPES, +} from '@typescript-eslint/experimental-utils'; +import * as util from '../util'; + +type Options = [ + { + allowTypedFunctionExpressions?: boolean; + allowHigherOrderFunctions?: boolean; + allowDirectConstAssertionInArrowFunctions?: boolean; + allowedNames?: string[]; + }, +]; +type MessageIds = 'missingReturnType' | 'missingArgType'; + +export default util.createRule({ + name: 'explicit-module-boundary-types', + meta: { + type: 'problem', + docs: { + description: + "Require explicit return and argument types on exported functions' and classes' public class methods", + category: 'Stylistic Issues', + recommended: false, + }, + messages: { + missingReturnType: 'Missing return type on function.', + missingArgType: "Argument '{{name}}' should be typed.", + }, + schema: [ + { + type: 'object', + properties: { + allowTypedFunctionExpressions: { + type: 'boolean', + }, + allowHigherOrderFunctions: { + type: 'boolean', + }, + allowDirectConstAssertionInArrowFunctions: { + type: 'boolean', + }, + allowedNames: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [ + { + allowTypedFunctionExpressions: true, + allowHigherOrderFunctions: true, + allowDirectConstAssertionInArrowFunctions: true, + allowedNames: [], + }, + ], + create(context, [options]) { + const sourceCode = context.getSourceCode(); + + /** + * Returns start column position + * @param node + */ + function getLocStart( + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression, + ): TSESTree.LineAndColumnData { + /* highlight method name */ + const parent = node.parent; + if ( + parent && + (parent.type === AST_NODE_TYPES.MethodDefinition || + (parent.type === AST_NODE_TYPES.Property && parent.method)) + ) { + return parent.loc.start; + } + + return node.loc.start; + } + + /** + * Returns end column position + * @param node + */ + function getLocEnd( + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression, + ): TSESTree.LineAndColumnData { + /* highlight `=>` */ + if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) { + return sourceCode.getTokenBefore( + node.body, + token => + token.type === AST_TOKEN_TYPES.Punctuator && token.value === '=>', + )!.loc.end; + } + + return sourceCode.getTokenBefore(node.body!)!.loc.end; + } + + /** + * Checks if a node is a constructor. + * @param node The node to check + */ + function isConstructor(node: TSESTree.Node | undefined): boolean { + return ( + !!node && + node.type === AST_NODE_TYPES.MethodDefinition && + node.kind === 'constructor' + ); + } + + /** + * Checks if a node is a setter. + */ + function isSetter(node: TSESTree.Node | undefined): boolean { + return ( + !!node && + (node.type === AST_NODE_TYPES.MethodDefinition || + node.type === AST_NODE_TYPES.Property) && + node.kind === 'set' + ); + } + + /** + * Checks if a node is a variable declarator with a type annotation. + * `const x: Foo = ...` + */ + function isVariableDeclaratorWithTypeAnnotation( + node: TSESTree.Node, + ): boolean { + return ( + node.type === AST_NODE_TYPES.VariableDeclarator && + !!node.id.typeAnnotation + ); + } + + /** + * Checks if a node is a class property with a type annotation. + * `public x: Foo = ...` + */ + function isClassPropertyWithTypeAnnotation(node: TSESTree.Node): boolean { + return ( + node.type === AST_NODE_TYPES.ClassProperty && !!node.typeAnnotation + ); + } + + /** + * Checks if a node belongs to: + * new Foo(() => {}) + * ^^^^^^^^ + */ + function isConstructorArgument(parent: TSESTree.Node): boolean { + return parent.type === AST_NODE_TYPES.NewExpression; + } + + /** + * Checks if a node is a type cast + * `(() => {}) as Foo` + * `(() => {})` + */ + function isTypeCast(node: TSESTree.Node): boolean { + return ( + node.type === AST_NODE_TYPES.TSAsExpression || + node.type === AST_NODE_TYPES.TSTypeAssertion + ); + } + + /** + * Checks if a node belongs to: + * `const x: Foo = { prop: () => {} }` + * `const x = { prop: () => {} } as Foo` + * `const x = { prop: () => {} }` + */ + function isPropertyOfObjectWithType( + property: TSESTree.Node | undefined, + ): boolean { + if (!property || property.type !== AST_NODE_TYPES.Property) { + return false; + } + const objectExpr = property.parent; // this shouldn't happen, checking just in case + /* istanbul ignore if */ if ( + !objectExpr || + objectExpr.type !== AST_NODE_TYPES.ObjectExpression + ) { + return false; + } + + const parent = objectExpr.parent; // this shouldn't happen, checking just in case + /* istanbul ignore if */ if (!parent) { + return false; + } + + return ( + isTypeCast(parent) || + isClassPropertyWithTypeAnnotation(parent) || + isVariableDeclaratorWithTypeAnnotation(parent) || + isFunctionArgument(parent) + ); + } + + function isUnexported(node: TSESTree.Node | undefined): boolean { + while (node) { + if ( + node.type === AST_NODE_TYPES.ExportDefaultDeclaration || + node.type === AST_NODE_TYPES.ExportNamedDeclaration || + node.type === AST_NODE_TYPES.ExportSpecifier + ) { + return false; + } + + node = node.parent; + } + + return true; + } + + function isPrivateMethod( + node: TSESTree.MethodDefinition | TSESTree.TSAbstractMethodDefinition, + ): boolean { + return node.accessibility === 'private'; + } + + /** + * Checks if a function belongs to: + * `() => () => ...` + * `() => function () { ... }` + * `() => { return () => ... }` + * `() => { return function () { ... } }` + * `function fn() { return () => ... }` + * `function fn() { return function() { ... } }` + */ + function doesImmediatelyReturnFunctionExpression({ + body, + }: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression): boolean { + // Should always have a body; really checking just in case + /* istanbul ignore if */ if (!body) { + return false; + } + + // Check if body is a block with a single statement + if ( + body.type === AST_NODE_TYPES.BlockStatement && + body.body.length === 1 + ) { + const [statement] = body.body; + + // Check if that statement is a return statement with an argument + if ( + statement.type === AST_NODE_TYPES.ReturnStatement && + !!statement.argument + ) { + // If so, check that returned argument as body + body = statement.argument; + } + } + + // Check if the body being returned is a function expression + return ( + body.type === AST_NODE_TYPES.ArrowFunctionExpression || + body.type === AST_NODE_TYPES.FunctionExpression + ); + } + + /** + * Checks if a node belongs to: + * `foo(() => 1)` + */ + function isFunctionArgument( + parent: TSESTree.Node, + callee?: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression, + ): boolean { + return ( + (parent.type === AST_NODE_TYPES.CallExpression || + parent.type === AST_NODE_TYPES.OptionalCallExpression) && + // make sure this isn't an IIFE + parent.callee !== callee + ); + } + + /** + * Checks if a function belongs to: + * `() => ({ action: 'xxx' }) as const` + */ + function returnsConstAssertionDirectly( + node: TSESTree.ArrowFunctionExpression, + ): boolean { + const { body } = node; + if (body.type === AST_NODE_TYPES.TSAsExpression) { + const { typeAnnotation } = body; + if (typeAnnotation.type === AST_NODE_TYPES.TSTypeReference) { + const { typeName } = typeAnnotation; + if ( + typeName.type === AST_NODE_TYPES.Identifier && + typeName.name === 'const' + ) { + return true; + } + } + } + + return false; + } + + /** + * Checks if a function declaration/expression has a return type. + */ + function checkFunctionReturnType( + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression, + ): void { + const paramIdentifiers = node.params.filter( + param => param.type === AST_NODE_TYPES.Identifier, + ) as TSESTree.Identifier[]; + const untypedArgs = paramIdentifiers.filter(isArgumentUntyped); + if (untypedArgs.length) { + untypedArgs.forEach(untypedArg => + context.report({ + node, + messageId: 'missingArgType', + data: { + name: untypedArg.name, + }, + }), + ); + } + + if (isAllowedName(node.parent)) { + return; + } + + if (isUnexported(node.parent)) { + return; + } + + if ( + options.allowHigherOrderFunctions && + doesImmediatelyReturnFunctionExpression(node) + ) { + return; + } + + if ( + node.returnType || + isConstructor(node.parent) || + isSetter(node.parent) + ) { + return; + } + + context.report({ + node, + loc: { start: getLocStart(node), end: getLocEnd(node) }, + messageId: 'missingReturnType', + }); + } + + /** + * Checks if a function declaration/expression has a return type. + */ + function checkFunctionExpressionReturnType( + node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression, + ): void { + // Should always have a parent; checking just in case + /* istanbul ignore else */ if (node.parent) { + if ( + node.parent.type === AST_NODE_TYPES.MethodDefinition && + isPrivateMethod(node.parent) + ) { + return; + } + + if (options.allowTypedFunctionExpressions) { + if ( + isTypeCast(node.parent) || + isVariableDeclaratorWithTypeAnnotation(node.parent) || + isClassPropertyWithTypeAnnotation(node.parent) || + isPropertyOfObjectWithType(node.parent) || + isFunctionArgument(node.parent, node) || + isConstructorArgument(node.parent) + ) { + return; + } + } + } + + if ( + node.type === AST_NODE_TYPES.ArrowFunctionExpression && + options.allowDirectConstAssertionInArrowFunctions && + returnsConstAssertionDirectly(node) + ) { + return; + } + + checkFunctionReturnType(node); + } + + /** + * Checks if a function name is allowed and should not be checked. + */ + function isAllowedName(node: TSESTree.Node | undefined): boolean { + if (!node || !options.allowedNames || !options.allowedNames.length) { + return false; + } + + if (node.type === AST_NODE_TYPES.VariableDeclarator) { + return ( + node.id.type === AST_NODE_TYPES.Identifier && + options.allowedNames.includes(node.id.name) + ); + } else if ( + node.type === AST_NODE_TYPES.MethodDefinition || + node.type === AST_NODE_TYPES.TSAbstractMethodDefinition + ) { + if ( + node.key.type === AST_NODE_TYPES.Literal && + typeof node.key.value === 'string' + ) { + return options.allowedNames.includes(node.key.value); + } + if ( + node.key.type === AST_NODE_TYPES.TemplateLiteral && + node.key.expressions.length === 0 + ) { + return options.allowedNames.includes(node.key.quasis[0].value.raw); + } + if (!node.computed && node.key.type === AST_NODE_TYPES.Identifier) { + return options.allowedNames.includes(node.key.name); + } + } + + return false; + } + + function isArgumentUntyped(node: TSESTree.Identifier): boolean { + return ( + !node.typeAnnotation || + node.typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSAnyKeyword + ); + } + + return { + ArrowFunctionExpression: checkFunctionExpressionReturnType, + FunctionDeclaration: checkFunctionReturnType, + FunctionExpression: checkFunctionExpressionReturnType, + }; + }, +}); diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index de4aa662ea32..1caa90bb46ef 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -11,6 +11,7 @@ import consistentTypeDefinitions from './consistent-type-definitions'; import defaultParamLast from './default-param-last'; import explicitFunctionReturnType from './explicit-function-return-type'; import explicitMemberAccessibility from './explicit-member-accessibility'; +import explicitModuleBoundaryTypes from './explicit-module-boundary-types'; import funcCallSpacing from './func-call-spacing'; import genericTypeNaming from './generic-type-naming'; import indent from './indent'; @@ -92,6 +93,7 @@ export default { 'default-param-last': defaultParamLast, 'explicit-function-return-type': explicitFunctionReturnType, 'explicit-member-accessibility': explicitMemberAccessibility, + 'explicit-module-boundary-types': explicitModuleBoundaryTypes, 'func-call-spacing': funcCallSpacing, 'generic-type-naming': genericTypeNaming, indent: indent, diff --git a/packages/eslint-plugin/src/rules/no-untyped-public-signature.ts b/packages/eslint-plugin/src/rules/no-untyped-public-signature.ts index 830a245760a1..303c1336b637 100644 --- a/packages/eslint-plugin/src/rules/no-untyped-public-signature.ts +++ b/packages/eslint-plugin/src/rules/no-untyped-public-signature.ts @@ -11,6 +11,8 @@ type Options = [{ ignoredMethods: string[] }]; export default util.createRule({ name: 'no-untyped-public-signature', meta: { + deprecated: true, + replacedBy: ['explicit-module-boundary-types'], docs: { description: 'Disallow untyped public methods', category: 'Best Practices', diff --git a/packages/eslint-plugin/tests/rules/explicit-module-boundary-types.test.ts b/packages/eslint-plugin/tests/rules/explicit-module-boundary-types.test.ts new file mode 100644 index 000000000000..f67218588d76 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/explicit-module-boundary-types.test.ts @@ -0,0 +1,802 @@ +import rule from '../../src/rules/explicit-module-boundary-types'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('explicit-module-boundary-types', rule, { + valid: [ + { + filename: 'test.ts', + code: ` +function test(): void { + return; +} + `, + }, + { + filename: 'test.ts', + code: ` +export function test(): void { + return; +} + `, + }, + { + filename: 'test.ts', + code: ` +export var fn = function(): number { + return 1; +}; + `, + }, + { + filename: 'test.ts', + code: ` +export var arrowFn = (): string => 'test'; + `, + }, + { + filename: 'test.ts', + code: ` +class Test { + constructor() {} + get prop() { + return 1; + } + set prop() {} + method() { + return; + } + arrow = (): string => 'arrow'; +} + `, + }, + { + filename: 'test.ts', + code: ` +export class Test { + constructor() {} + get prop(): number { + return 1; + } + set prop() {} + private method(one) { + return; + } + arrow = (): string => 'arrow'; +} + `, + }, + { + filename: 'test.ts', + code: ` +export var arrowFn: Foo = () => 'test'; + `, + options: [ + { + allowTypedFunctionExpressions: true, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export var funcExpr: Foo = function() { return 'test'; }; + `, + options: [ + { + allowTypedFunctionExpressions: true, + }, + ], + }, + { + filename: 'test.ts', + code: `const x = (() => {}) as Foo`, + options: [{ allowTypedFunctionExpressions: true }], + }, + { + filename: 'test.ts', + code: `const x = (() => {})`, + options: [{ allowTypedFunctionExpressions: true }], + }, + { + filename: 'test.ts', + code: ` +export const x = { + foo: () => {}, +} as Foo + `, + options: [{ allowTypedFunctionExpressions: true }], + }, + { + filename: 'test.ts', + code: ` +export const x = { + foo: () => {}, +} + `, + options: [{ allowTypedFunctionExpressions: true }], + }, + { + filename: 'test.ts', + code: ` +export const x: Foo = { + foo: () => {}, +} + `, + options: [{ allowTypedFunctionExpressions: true }], + }, + // https://github.com/typescript-eslint/typescript-eslint/issues/484 + { + filename: 'test.ts', + code: ` +type MethodType = () => void; + +export class App { + public method: MethodType = () => {} +} + `, + options: [{ allowTypedFunctionExpressions: true }], + }, + // https://github.com/typescript-eslint/typescript-eslint/issues/525 + { + filename: 'test.ts', + code: ` +export const myObj = { + set myProp(val: number) { + this.myProp = val; + }, +}; + `, + }, + { + filename: 'test.ts', + code: ` +export default () => (): void => {}; + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export default () => function (): void {}; + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export default () => { return (): void => {} }; + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export default () => { return function (): void {} }; + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export function fn() { return (): void => {} }; + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export function fn() { return function (): void {} }; + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export function FunctionDeclaration() { + return function FunctionExpression_Within_FunctionDeclaration() { + return function FunctionExpression_Within_FunctionExpression() { + return () => { // ArrowFunctionExpression_Within_FunctionExpression + return () => // ArrowFunctionExpression_Within_ArrowFunctionExpression + (): number => 1 // ArrowFunctionExpression_Within_ArrowFunctionExpression_WithNoBody + } + } + } +} + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export default () => () => { return (): void => { return; } }; + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export class Accumulator { + private count: number = 0; + + public accumulate(fn: () => number): void { + this.count += fn(); + } +} + +new Accumulator().accumulate(() => 1); + `, + options: [ + { + allowTypedFunctionExpressions: true, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export const func1 = (value: number) => (({ type: "X", value }) as const); +export const func2 = (value: number) => ({ type: "X", value } as const); +export const func3 = (value: number) => (x as const); +export const func4 = (value: number) => x as const; + `, + options: [ + { + allowDirectConstAssertionInArrowFunctions: true, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export const func1 = (value: string) => value; +export const func2 = (value: number) => ({ type: "X", value }); + `, + options: [ + { + allowedNames: ['func1', 'func2'], + }, + ], + }, + { + filename: 'test.ts', + code: ` +export class Test { + constructor() {} + get prop() { + return 1; + } + set prop() {} + method() { + return; + } + arrow = (): string => 'arrow'; +} + `, + options: [ + { + allowedNames: ['prop', 'method'], + }, + ], + }, + ], + invalid: [ + { + filename: 'test.ts', + code: ` +export function test( + a: number, + b: number, +) { + return; +} + `, + errors: [ + { + messageId: 'missingReturnType', + line: 2, + endLine: 5, + column: 8, + endColumn: 2, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export function test() { + return; +} + `, + errors: [ + { + messageId: 'missingReturnType', + line: 2, + endLine: 2, + column: 8, + endColumn: 23, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export var fn = function() { + return 1; +}; + `, + errors: [ + { + messageId: 'missingReturnType', + line: 2, + endLine: 2, + column: 17, + endColumn: 27, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export var arrowFn = () => 'test'; + `, + errors: [ + { + messageId: 'missingReturnType', + line: 2, + endLine: 2, + column: 22, + endColumn: 27, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export class Test { + constructor() {} + get prop() { + return 1; + } + set prop() {} + method() { + return; + } + arrow = (arg) => 'arrow'; + private method() { + return; + } +} + `, + errors: [ + { + messageId: 'missingReturnType', + line: 4, + endLine: 4, + column: 3, + endColumn: 13, + }, + { + messageId: 'missingReturnType', + line: 8, + endLine: 8, + column: 3, + endColumn: 11, + }, + { + messageId: 'missingArgType', + line: 11, + endLine: 11, + column: 11, + endColumn: 27, + }, + { + messageId: 'missingReturnType', + line: 11, + endLine: 11, + column: 11, + endColumn: 19, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export class Foo { + public a = () => {}; + public b = function () {}; + public c = function test() {}; + + static d = () => {}; + static e = function () {}; +} + `, + errors: [ + { + messageId: 'missingReturnType', + line: 3, + endLine: 3, + column: 14, + endColumn: 19, + }, + { + messageId: 'missingReturnType', + line: 4, + endLine: 4, + column: 14, + endColumn: 25, + }, + { + messageId: 'missingReturnType', + line: 5, + endLine: 5, + column: 14, + endColumn: 29, + }, + { + messageId: 'missingReturnType', + line: 7, + endLine: 7, + column: 14, + endColumn: 19, + }, + { + messageId: 'missingReturnType', + line: 8, + endLine: 8, + column: 14, + endColumn: 25, + }, + ], + }, + { + filename: 'test.ts', + code: "export var arrowFn = () => 'test';", + options: [{ allowTypedFunctionExpressions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 22, + endColumn: 27, + }, + ], + }, + { + filename: 'test.ts', + code: "export var funcExpr = function() { return 'test'; };", + options: [{ allowTypedFunctionExpressions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 23, + endColumn: 33, + }, + ], + }, + { + filename: 'test.ts', + code: 'export const x = (() => {}) as Foo', + options: [{ allowTypedFunctionExpressions: false }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 19, + endColumn: 24, + }, + ], + }, + { + filename: 'test.ts', + code: ` +interface Foo {} +export const x = { + foo: () => {}, +} as Foo + `, + options: [{ allowTypedFunctionExpressions: false }], + errors: [ + { + messageId: 'missingReturnType', + line: 4, + endLine: 4, + column: 8, + endColumn: 13, + }, + ], + }, + { + filename: 'test.ts', + code: ` +interface Foo {} +export const x: Foo = { + foo: () => {}, +} + `, + options: [{ allowTypedFunctionExpressions: false }], + errors: [ + { + messageId: 'missingReturnType', + line: 4, + endLine: 4, + column: 8, + endColumn: 13, + }, + ], + }, + { + filename: 'test.ts', + code: 'export default () => () => {};', + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 22, + endColumn: 27, + }, + ], + }, + { + filename: 'test.ts', + code: 'export default () => function () {};', + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 22, + endColumn: 33, + }, + ], + }, + { + filename: 'test.ts', + code: 'export default () => { return () => {} };', + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 31, + endColumn: 36, + }, + ], + }, + { + filename: 'test.ts', + code: 'export default () => { return function () {} };', + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 31, + endColumn: 42, + }, + ], + }, + { + filename: 'test.ts', + code: 'export function fn() { return () => {} };', + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 31, + endColumn: 36, + }, + ], + }, + { + filename: 'test.ts', + code: 'export function fn() { return function () {} };', + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 31, + endColumn: 42, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export function FunctionDeclaration() { + return function FunctionExpression_Within_FunctionDeclaration() { + return function FunctionExpression_Within_FunctionExpression() { + return () => { // ArrowFunctionExpression_Within_FunctionExpression + return () => // ArrowFunctionExpression_Within_ArrowFunctionExpression + () => 1 // ArrowFunctionExpression_Within_ArrowFunctionExpression_WithNoBody + } + } + } +} + `, + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 7, + endLine: 7, + column: 11, + endColumn: 16, + }, + ], + }, + { + filename: 'test.ts', + code: 'export default () => () => { return () => { return; } };', + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 37, + endColumn: 42, + }, + ], + }, + { + filename: 'test.ts', + code: 'export default (() => true)()', + options: [ + { + allowTypedFunctionExpressions: false, + }, + ], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 17, + endColumn: 22, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export const func1 = (value: number) => ({ type: "X", value } as any); +export const func2 = (value: number) => ({ type: "X", value } as Action); + `, + options: [ + { + allowDirectConstAssertionInArrowFunctions: true, + }, + ], + errors: [ + { + messageId: 'missingReturnType', + line: 2, + endLine: 2, + column: 22, + endColumn: 40, + }, + { + messageId: 'missingReturnType', + line: 3, + endLine: 3, + column: 22, + endColumn: 40, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export const func = (value: number) => ({ type: "X", value } as const); + `, + options: [ + { + allowDirectConstAssertionInArrowFunctions: false, + }, + ], + errors: [ + { + messageId: 'missingReturnType', + line: 2, + endLine: 2, + column: 21, + endColumn: 39, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export class Test { + constructor() {} + get prop() { + return 1; + } + set prop() {} + method() { + return; + } + arrow = (): string => 'arrow'; +} + `, + options: [ + { + allowedNames: ['prop'], + }, + ], + errors: [ + { + messageId: 'missingReturnType', + line: 8, + endLine: 8, + column: 3, + endColumn: 11, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export const func1 = (value: number) => value; +export const func2 = (value: number) => value; + `, + options: [ + { + allowedNames: ['func2'], + }, + ], + errors: [ + { + messageId: 'missingReturnType', + line: 2, + endLine: 2, + column: 22, + endColumn: 40, + }, + ], + }, + { + filename: 'test.ts', + code: 'export function fn(test): string { return "123" };', + errors: [ + { + messageId: 'missingArgType', + line: 1, + endLine: 1, + column: 8, + endColumn: 50, + }, + ], + }, + { + filename: 'test.ts', + code: 'export const fn = (one: number, two): string => "123";', + errors: [ + { + messageId: 'missingArgType', + line: 1, + endLine: 1, + column: 19, + endColumn: 54, + }, + ], + }, + ], +}); From 9f9a5c5552b41fe1a46eb127bc10fbf9c41a2197 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Wed, 15 Jan 2020 21:35:44 -0800 Subject: [PATCH 10/17] docs(eslint-plugin): clearly document extension rules (#1456) --- docs/getting-started/linting/FAQ.md | 20 ++++++ packages/eslint-plugin/README.md | 61 ++++++++++++------- packages/eslint-plugin/src/configs/all.json | 1 + .../eslint-plugin/src/rules/brace-style.ts | 1 + packages/eslint-plugin/src/rules/camelcase.ts | 1 + .../src/rules/default-param-last.ts | 1 + .../src/rules/func-call-spacing.ts | 1 + packages/eslint-plugin/src/rules/indent.ts | 1 + .../src/rules/no-array-constructor.ts | 1 + .../src/rules/no-empty-function.ts | 1 + .../src/rules/no-extra-parens.ts | 1 + .../eslint-plugin/src/rules/no-extra-semi.ts | 1 + .../src/rules/no-magic-numbers.ts | 1 + .../src/rules/no-unused-expressions.ts | 1 + .../eslint-plugin/src/rules/no-unused-vars.ts | 1 + .../src/rules/no-use-before-define.ts | 1 + .../src/rules/no-useless-constructor.ts | 1 + packages/eslint-plugin/src/rules/quotes.ts | 1 + .../eslint-plugin/src/rules/require-await.ts | 1 + .../eslint-plugin/src/rules/return-await.ts | 1 + packages/eslint-plugin/src/rules/semi.ts | 1 + .../src/rules/space-before-function-paren.ts | 1 + packages/eslint-plugin/tests/configs.test.ts | 50 +++++++++++++-- packages/eslint-plugin/tests/docs.test.ts | 55 +++++++++++++---- .../eslint-plugin/tools/generate-configs.ts | 40 ++++++------ .../experimental-utils/src/ts-eslint/Rule.ts | 6 ++ 26 files changed, 187 insertions(+), 65 deletions(-) diff --git a/docs/getting-started/linting/FAQ.md b/docs/getting-started/linting/FAQ.md index 290c57984286..f5585b170742 100644 --- a/docs/getting-started/linting/FAQ.md +++ b/docs/getting-started/linting/FAQ.md @@ -5,6 +5,7 @@ - [My linting seems really slow](#my-linting-seems-really-slow) - [I get errors telling me "The file must be included in at least one of the projects provided"](#i-get-errors-telling-me-"the-file-must-be-included-in-at-least-one-of-the-projects-provided") - [I use a framework (like Vue) that requires custom file extensions, and I get errors like "You should add `parserOptions.extraFileExtensions` to your config"](<#i-use-a-framework-(like-vue)-that-requires-custom-file-extensions,-and-i-get-errors-like-"you-should-add-`parserOptions.extraFileExtensions`-to-your-config">) +- [I am using a rule from ESLint core, and it doesn't work correctly with TypeScript code](#i-am-using-a-rule-from-eslint-core,-and-it-doesn't-work-correctly-with-typescript-code) --- @@ -81,3 +82,22 @@ You can use `parserOptions.extraFileExtensions` to specify an array of non-TypeS ``` --- + +## I am using a rule from ESLint core, and it doesn't work correctly with TypeScript code + +This is a pretty common thing because TypeScript adds new features that ESLint doesn't know about. + +The first step is to [check our list of "extension" rules here](../../../packages/eslint-plugin/README.md#extension-rules). An extension rule is simply a rule which extends the base ESLint rules to support TypeScript syntax. If you find it in there, give it a go to see if it works for you. You can configure it by disabling the base rule, and turning on the extension rule. Here's an example with the `semi` rule: + +```json +{ + "rules": { + "semi": "off", + "@typescript-eslint/semi": "error" + } +} +``` + +If you don't find an existing extension rule, or the extension rule doesn't work for your case, then you can go ahead and check our issues. [The contributing guide outlines the best way to raise an issue](../../../CONTRIBUTING.md#raising-issues). + +--- diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 0f81cf64216e..2111de5cee39 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -88,7 +88,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int ## Supported Rules - + **Key**: :heavy_check_mark: = recommended, :wrench: = fixable, :thought_balloon: = requires type information @@ -100,32 +100,23 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/await-thenable`](./docs/rules/await-thenable.md) | Disallows awaiting a value that is not a Thenable | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/ban-ts-ignore`](./docs/rules/ban-ts-ignore.md) | Bans “// @ts-ignore” comments from being used | :heavy_check_mark: | | | | [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Bans specific types from being used | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | | | [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforces consistent usage of type assertions | :heavy_check_mark: | | | | [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Consistent with type definition either `interface` or `type` | | :wrench: | | -| [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | | | [`@typescript-eslint/explicit-function-return-type`](./docs/rules/explicit-function-return-type.md) | Require explicit return types on functions and class methods | :heavy_check_mark: | | | | [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | | | | [`@typescript-eslint/explicit-module-boundary-types`](./docs/rules/explicit-module-boundary-types.md) | Require explicit return and argument types on exported functions' and classes' public class methods | | | | -| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | | -| [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | | | [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order | | | | | [`@typescript-eslint/naming-convention`](./docs/rules/naming-convention.md) | Enforces naming conventions for everything across a codebase | | | :thought_balloon: | -| [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-dynamic-delete`](./docs/rules/no-dynamic-delete.md) | Disallow the delete operator with computed key expressions | | :wrench: | | -| [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | | | [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-extra-non-null-assertion`](./docs/rules/no-extra-non-null-assertion.md) | Disallow extra non-null assertion | | | | -| [`@typescript-eslint/no-extra-parens`](./docs/rules/no-extra-parens.md) | Disallow unnecessary parentheses | | :wrench: | | -| [`@typescript-eslint/no-extra-semi`](./docs/rules/no-extra-semi.md) | Disallow unnecessary semicolons | | :wrench: | | | [`@typescript-eslint/no-extraneous-class`](./docs/rules/no-extraneous-class.md) | Forbids the use of classes as namespaces | | | | | [`@typescript-eslint/no-floating-promises`](./docs/rules/no-floating-promises.md) | Requires Promise-like values to be handled appropriately | | | :thought_balloon: | | [`@typescript-eslint/no-for-in-array`](./docs/rules/no-for-in-array.md) | Disallow iterating over an array with a for-in loop | :heavy_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-implied-eval`](./docs/rules/no-implied-eval.md) | Disallow the use of `eval()`-like methods | | | :thought_balloon:| +| [`@typescript-eslint/no-implied-eval`](./docs/rules/no-implied-eval.md) | Disallow the use of `eval()`-like methods | | | :thought_balloon:| | [`@typescript-eslint/no-inferrable-types`](./docs/rules/no-inferrable-types.md) | Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/no-magic-numbers`](./docs/rules/no-magic-numbers.md) | Disallow magic numbers | | | | | [`@typescript-eslint/no-misused-new`](./docs/rules/no-misused-new.md) | Enforce valid definition of `new` and `constructor` | :heavy_check_mark: | | | | [`@typescript-eslint/no-misused-promises`](./docs/rules/no-misused-promises.md) | Avoid using promises in places not designed to handle them | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/no-namespace`](./docs/rules/no-namespace.md) | Disallow the use of custom TypeScript modules and namespaces | :heavy_check_mark: | | | @@ -139,11 +130,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/no-unnecessary-qualifier`](./docs/rules/no-unnecessary-qualifier.md) | Warns when a namespace qualifier is unnecessary | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-type-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Enforces that type arguments will not be used if not required | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | :heavy_check_mark: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-unused-expressions`](./docs/rules/no-unused-expressions.md) | Disallow unused expressions | | | | -| [`@typescript-eslint/no-unused-vars`](./docs/rules/no-unused-vars.md) | Disallow unused variables | :heavy_check_mark: | | | | [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments | | | :thought_balloon: | -| [`@typescript-eslint/no-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | :heavy_check_mark: | | | -| [`@typescript-eslint/no-useless-constructor`](./docs/rules/no-useless-constructor.md) | Disallow unnecessary constructors | | | | | [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | | | [`@typescript-eslint/prefer-for-of`](./docs/rules/prefer-for-of.md) | Prefer a ‘for-of’ loop over a standard ‘for’ loop if the index is only used to access the array being iterated | | | | | [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures | | :wrench: | | @@ -155,14 +142,9 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/prefer-string-starts-ends-with`](./docs/rules/prefer-string-starts-ends-with.md) | Enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings | :heavy_check_mark: | :wrench: | :thought_balloon: | | [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async | | | :thought_balloon: | -| [`@typescript-eslint/quotes`](./docs/rules/quotes.md) | Enforce the consistent use of either backticks, double, or single quotes | | :wrench: | | -| [`@typescript-eslint/require-array-sort-compare`](./docs/rules/require-array-sort-compare.md) | Requires `Array#sort` calls to always provide a `compareFunction` | | | :thought_balloon: | -| [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :heavy_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/require-array-sort-compare`](./docs/rules/require-array-sort-compare.md) | Requires `Array#sort` calls to always provide a `compareFunction` | | | :thought_balloon: | | [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string | | | :thought_balloon: | | [`@typescript-eslint/restrict-template-expressions`](./docs/rules/restrict-template-expressions.md) | Enforce template literal expressions to be of string type | | | :thought_balloon: | -| [`@typescript-eslint/return-await`](./docs/rules/return-await.md) | Enforces consistent returning of awaited values | | | :thought_balloon: | -| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | | -| [`@typescript-eslint/space-before-function-paren`](./docs/rules/space-before-function-paren.md) | Enforces consistent spacing before function parenthesis | | :wrench: | | | [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: | | [`@typescript-eslint/triple-slash-reference`](./docs/rules/triple-slash-reference.md) | Sets preference level for triple slash directives versus ES6-style import declarations | :heavy_check_mark: | | | | [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | :heavy_check_mark: | :wrench: | | @@ -170,8 +152,41 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforces unbound methods are called with their expected scope | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter | | | | - + + +### Extension Rules + +In some cases, ESLint provides a rule itself, but it doesn't support TypeScript syntax; either it crashes, or it ignores the syntax, or it falsely reports against it. +In these cases, we create what we call an extension rule; a rule within our plugin that has the same functionality, but also supports TypeScript. + + + +**Key**: :heavy_check_mark: = recommended, :wrench: = fixable, :thought_balloon: = requires type information + + +| Name | Description | :heavy_check_mark: | :wrench: | :thought_balloon: | +| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -------- | ----------------- | +| [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | | +| [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | | +| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | | +| [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | | +| [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | | +| [`@typescript-eslint/no-extra-parens`](./docs/rules/no-extra-parens.md) | Disallow unnecessary parentheses | | :wrench: | | +| [`@typescript-eslint/no-extra-semi`](./docs/rules/no-extra-semi.md) | Disallow unnecessary semicolons | | :wrench: | | +| [`@typescript-eslint/no-magic-numbers`](./docs/rules/no-magic-numbers.md) | Disallow magic numbers | | | | +| [`@typescript-eslint/no-unused-expressions`](./docs/rules/no-unused-expressions.md) | Disallow unused expressions | | | | +| [`@typescript-eslint/no-unused-vars`](./docs/rules/no-unused-vars.md) | Disallow unused variables | :heavy_check_mark: | | | +| [`@typescript-eslint/no-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | :heavy_check_mark: | | | +| [`@typescript-eslint/no-useless-constructor`](./docs/rules/no-useless-constructor.md) | Disallow unnecessary constructors | | | | +| [`@typescript-eslint/quotes`](./docs/rules/quotes.md) | Enforce the consistent use of either backticks, double, or single quotes | | :wrench: | | +| [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :heavy_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/return-await`](./docs/rules/return-await.md) | Enforces consistent returning of awaited values | | | :thought_balloon: | +| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | | +| [`@typescript-eslint/space-before-function-paren`](./docs/rules/space-before-function-paren.md) | Enforces consistent spacing before function parenthesis | | :wrench: | | + + ## Contributing -[See the contributing guide here](../../CONTRIBUTING.md) +[See the contributing guide here](../../CONTRIBUTING.md). diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 43dbe291f879..9f1f3c1816ee 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -81,6 +81,7 @@ "@typescript-eslint/require-await": "error", "@typescript-eslint/restrict-plus-operands": "error", "@typescript-eslint/restrict-template-expressions": "error", + "no-return-await": "off", "@typescript-eslint/return-await": "error", "semi": "off", "@typescript-eslint/semi": "error", diff --git a/packages/eslint-plugin/src/rules/brace-style.ts b/packages/eslint-plugin/src/rules/brace-style.ts index 5be461b8c23b..7107bdf4d1ba 100644 --- a/packages/eslint-plugin/src/rules/brace-style.ts +++ b/packages/eslint-plugin/src/rules/brace-style.ts @@ -18,6 +18,7 @@ export default createRule({ description: 'Enforce consistent brace style for blocks', category: 'Stylistic Issues', recommended: false, + extendsBaseRule: true, }, messages: baseRule.meta.messages, fixable: baseRule.meta.fixable, diff --git a/packages/eslint-plugin/src/rules/camelcase.ts b/packages/eslint-plugin/src/rules/camelcase.ts index 37047b9b4ff3..0c3ba619a42f 100644 --- a/packages/eslint-plugin/src/rules/camelcase.ts +++ b/packages/eslint-plugin/src/rules/camelcase.ts @@ -29,6 +29,7 @@ export default util.createRule({ description: 'Enforce camelCase naming convention', category: 'Stylistic Issues', recommended: 'error', + extendsBaseRule: true, }, deprecated: true, replacedBy: ['naming-convention'], diff --git a/packages/eslint-plugin/src/rules/default-param-last.ts b/packages/eslint-plugin/src/rules/default-param-last.ts index 0f0191155a47..956a88479752 100644 --- a/packages/eslint-plugin/src/rules/default-param-last.ts +++ b/packages/eslint-plugin/src/rules/default-param-last.ts @@ -12,6 +12,7 @@ export default createRule({ description: 'Enforce default parameters to be last', category: 'Best Practices', recommended: false, + extendsBaseRule: true, }, schema: [], messages: { diff --git a/packages/eslint-plugin/src/rules/func-call-spacing.ts b/packages/eslint-plugin/src/rules/func-call-spacing.ts index d76204c3fef5..8e11d58592be 100644 --- a/packages/eslint-plugin/src/rules/func-call-spacing.ts +++ b/packages/eslint-plugin/src/rules/func-call-spacing.ts @@ -19,6 +19,7 @@ export default util.createRule({ 'Require or disallow spacing between function identifiers and their invocations', category: 'Stylistic Issues', recommended: false, + extendsBaseRule: true, }, fixable: 'whitespace', schema: { diff --git a/packages/eslint-plugin/src/rules/indent.ts b/packages/eslint-plugin/src/rules/indent.ts index 9023939ac629..46cca6acd151 100644 --- a/packages/eslint-plugin/src/rules/indent.ts +++ b/packages/eslint-plugin/src/rules/indent.ts @@ -93,6 +93,7 @@ export default util.createRule({ category: 'Stylistic Issues', // too opinionated to be recommended recommended: false, + extendsBaseRule: true, }, fixable: 'whitespace', schema: baseRule.meta.schema, diff --git a/packages/eslint-plugin/src/rules/no-array-constructor.ts b/packages/eslint-plugin/src/rules/no-array-constructor.ts index 731cb960973c..dddae97f6633 100644 --- a/packages/eslint-plugin/src/rules/no-array-constructor.ts +++ b/packages/eslint-plugin/src/rules/no-array-constructor.ts @@ -12,6 +12,7 @@ export default util.createRule({ description: 'Disallow generic `Array` constructors', category: 'Stylistic Issues', recommended: 'error', + extendsBaseRule: true, }, fixable: 'code', messages: { diff --git a/packages/eslint-plugin/src/rules/no-empty-function.ts b/packages/eslint-plugin/src/rules/no-empty-function.ts index ce93c16c3894..5d85dd306bbf 100644 --- a/packages/eslint-plugin/src/rules/no-empty-function.ts +++ b/packages/eslint-plugin/src/rules/no-empty-function.ts @@ -42,6 +42,7 @@ export default util.createRule({ description: 'Disallow empty functions', category: 'Best Practices', recommended: 'error', + extendsBaseRule: true, }, schema: [schema], messages: baseRule.meta.messages, diff --git a/packages/eslint-plugin/src/rules/no-extra-parens.ts b/packages/eslint-plugin/src/rules/no-extra-parens.ts index 9398385a3af7..e3ab2395279b 100644 --- a/packages/eslint-plugin/src/rules/no-extra-parens.ts +++ b/packages/eslint-plugin/src/rules/no-extra-parens.ts @@ -20,6 +20,7 @@ export default util.createRule({ description: 'Disallow unnecessary parentheses', category: 'Possible Errors', recommended: false, + extendsBaseRule: true, }, fixable: 'code', schema: baseRule.meta.schema, diff --git a/packages/eslint-plugin/src/rules/no-extra-semi.ts b/packages/eslint-plugin/src/rules/no-extra-semi.ts index db18236979e6..6481b8ac7ed4 100644 --- a/packages/eslint-plugin/src/rules/no-extra-semi.ts +++ b/packages/eslint-plugin/src/rules/no-extra-semi.ts @@ -12,6 +12,7 @@ export default util.createRule({ description: 'Disallow unnecessary semicolons', category: 'Possible Errors', recommended: false, + extendsBaseRule: true, }, fixable: 'code', schema: baseRule.meta.schema, diff --git a/packages/eslint-plugin/src/rules/no-magic-numbers.ts b/packages/eslint-plugin/src/rules/no-magic-numbers.ts index 6576ad4b49f8..c285b0eb3024 100644 --- a/packages/eslint-plugin/src/rules/no-magic-numbers.ts +++ b/packages/eslint-plugin/src/rules/no-magic-numbers.ts @@ -20,6 +20,7 @@ export default util.createRule({ description: 'Disallow magic numbers', category: 'Best Practices', recommended: false, + extendsBaseRule: true, }, // Extend base schema with additional property to ignore TS numeric literal types schema: [ diff --git a/packages/eslint-plugin/src/rules/no-unused-expressions.ts b/packages/eslint-plugin/src/rules/no-unused-expressions.ts index b2eb6f8e30b7..05f6e3af1d5a 100644 --- a/packages/eslint-plugin/src/rules/no-unused-expressions.ts +++ b/packages/eslint-plugin/src/rules/no-unused-expressions.ts @@ -10,6 +10,7 @@ export default util.createRule({ description: 'Disallow unused expressions', category: 'Best Practices', recommended: false, + extendsBaseRule: true, }, schema: baseRule.meta.schema, messages: {}, diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index 46084af3a6b5..b939f8214a92 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -13,6 +13,7 @@ export default util.createRule({ description: 'Disallow unused variables', category: 'Variables', recommended: 'warn', + extendsBaseRule: true, }, schema: baseRule.meta.schema, messages: baseRule.meta.messages, diff --git a/packages/eslint-plugin/src/rules/no-use-before-define.ts b/packages/eslint-plugin/src/rules/no-use-before-define.ts index e3d5cb81437b..2c66ec1ee5af 100644 --- a/packages/eslint-plugin/src/rules/no-use-before-define.ts +++ b/packages/eslint-plugin/src/rules/no-use-before-define.ts @@ -178,6 +178,7 @@ export default util.createRule({ description: 'Disallow the use of variables before they are defined', category: 'Variables', recommended: 'error', + extendsBaseRule: true, }, messages: { noUseBeforeDefine: "'{{name}}' was used before it was defined.", diff --git a/packages/eslint-plugin/src/rules/no-useless-constructor.ts b/packages/eslint-plugin/src/rules/no-useless-constructor.ts index 3587c7953119..395ff28d45e1 100644 --- a/packages/eslint-plugin/src/rules/no-useless-constructor.ts +++ b/packages/eslint-plugin/src/rules/no-useless-constructor.ts @@ -51,6 +51,7 @@ export default util.createRule({ description: 'Disallow unnecessary constructors', category: 'Best Practices', recommended: false, + extendsBaseRule: true, }, schema: baseRule.meta.schema, messages: baseRule.meta.messages, diff --git a/packages/eslint-plugin/src/rules/quotes.ts b/packages/eslint-plugin/src/rules/quotes.ts index 6a8e91814c1b..a266e912fdf3 100644 --- a/packages/eslint-plugin/src/rules/quotes.ts +++ b/packages/eslint-plugin/src/rules/quotes.ts @@ -17,6 +17,7 @@ export default util.createRule({ 'Enforce the consistent use of either backticks, double, or single quotes', category: 'Stylistic Issues', recommended: false, + extendsBaseRule: true, }, fixable: 'code', messages: baseRule.meta.messages, diff --git a/packages/eslint-plugin/src/rules/require-await.ts b/packages/eslint-plugin/src/rules/require-await.ts index b9949659daaf..32bee2f4cef4 100644 --- a/packages/eslint-plugin/src/rules/require-await.ts +++ b/packages/eslint-plugin/src/rules/require-await.ts @@ -19,6 +19,7 @@ export default util.createRule({ category: 'Best Practices', recommended: 'error', requiresTypeChecking: true, + extendsBaseRule: true, }, schema: baseRule.meta.schema, messages: baseRule.meta.messages, diff --git a/packages/eslint-plugin/src/rules/return-await.ts b/packages/eslint-plugin/src/rules/return-await.ts index 52ffb052c7e8..f77d80fe5d40 100644 --- a/packages/eslint-plugin/src/rules/return-await.ts +++ b/packages/eslint-plugin/src/rules/return-await.ts @@ -14,6 +14,7 @@ export default util.createRule({ category: 'Best Practices', recommended: false, requiresTypeChecking: true, + extendsBaseRule: 'no-return-await', }, type: 'problem', messages: { diff --git a/packages/eslint-plugin/src/rules/semi.ts b/packages/eslint-plugin/src/rules/semi.ts index 8d1ada467e3c..f35b93d98594 100644 --- a/packages/eslint-plugin/src/rules/semi.ts +++ b/packages/eslint-plugin/src/rules/semi.ts @@ -18,6 +18,7 @@ export default util.createRule({ category: 'Stylistic Issues', // too opinionated to be recommended recommended: false, + extendsBaseRule: true, }, fixable: 'code', schema: baseRule.meta.schema, diff --git a/packages/eslint-plugin/src/rules/space-before-function-paren.ts b/packages/eslint-plugin/src/rules/space-before-function-paren.ts index ab2640dcc360..e4b4cb2f960d 100644 --- a/packages/eslint-plugin/src/rules/space-before-function-paren.ts +++ b/packages/eslint-plugin/src/rules/space-before-function-paren.ts @@ -26,6 +26,7 @@ export default util.createRule({ description: 'Enforces consistent spacing before function parenthesis', category: 'Stylistic Issues', recommended: false, + extendsBaseRule: true, }, fixable: 'whitespace', schema: [ diff --git a/packages/eslint-plugin/tests/configs.test.ts b/packages/eslint-plugin/tests/configs.test.ts index eea9079f8ba3..f034fd86ccfd 100644 --- a/packages/eslint-plugin/tests/configs.test.ts +++ b/packages/eslint-plugin/tests/configs.test.ts @@ -1,6 +1,19 @@ import rules from '../src/rules'; import plugin from '../src/index'; +const RULE_NAME_PREFIX = '@typescript-eslint/'; +const EXTENSION_RULES = Object.entries(rules) + .filter(([, rule]) => rule.meta.docs.extendsBaseRule) + .map( + ([ruleName, rule]) => + [ + `${RULE_NAME_PREFIX}${ruleName}`, + typeof rule.meta.docs.extendsBaseRule === 'string' + ? rule.meta.docs.extendsBaseRule + : ruleName, + ] as const, + ); + function entriesToObject(value: [string, T][]): Record { return value.reduce>((accum, [k, v]) => { accum[k] = v; @@ -14,10 +27,27 @@ function filterRules(values: Record): [string, string][] { ); } -const RULE_NAME_PREFIX = '@typescript-eslint/'; +function itHasBaseRulesOverriden( + unfilteredConfigRules: Record, +): void { + it('has the base rules overriden by the appropriate extension rules', () => { + const ruleNames = new Set(Object.keys(unfilteredConfigRules)); + EXTENSION_RULES.forEach(([ruleName, extRuleName]) => { + if (ruleNames.has(ruleName)) { + // this looks a little weird, but it provides the cleanest test output style + expect(unfilteredConfigRules).toMatchObject({ + ...unfilteredConfigRules, + [extRuleName]: 'off', + }); + } + }); + }); +} describe('all.json config', () => { - const configRules = filterRules(plugin.configs.all.rules); + const unfilteredConfigRules: Record = + plugin.configs.all.rules; + const configRules = filterRules(unfilteredConfigRules); // note: exclude deprecated rules, this config is allowed to change between minor versions const ruleConfigs = Object.entries(rules) .filter(([, rule]) => !rule.meta.deprecated) @@ -26,10 +56,14 @@ describe('all.json config', () => { it('contains all of the rules, excluding the deprecated ones', () => { expect(entriesToObject(ruleConfigs)).toEqual(entriesToObject(configRules)); }); + + itHasBaseRulesOverriden(unfilteredConfigRules); }); describe('recommended.json config', () => { - const configRules = filterRules(plugin.configs.recommended.rules); + const unfilteredConfigRules: Record = + plugin.configs.recommended.rules; + const configRules = filterRules(unfilteredConfigRules); // note: include deprecated rules so that the config doesn't change between major bumps const ruleConfigs = Object.entries(rules) .filter( @@ -45,12 +79,14 @@ describe('recommended.json config', () => { it("contains all recommended rules that don't require typechecking, excluding the deprecated ones", () => { expect(entriesToObject(ruleConfigs)).toEqual(entriesToObject(configRules)); }); + + itHasBaseRulesOverriden(unfilteredConfigRules); }); describe('recommended-requiring-type-checking.json config', () => { - const configRules = filterRules( - plugin.configs['recommended-requiring-type-checking'].rules, - ); + const unfilteredConfigRules: Record = + plugin.configs['recommended-requiring-type-checking'].rules; + const configRules = filterRules(unfilteredConfigRules); // note: include deprecated rules so that the config doesn't change between major bumps const ruleConfigs = Object.entries(rules) .filter( @@ -66,4 +102,6 @@ describe('recommended-requiring-type-checking.json config', () => { it('contains all recommended rules that require type checking, excluding the deprecated ones', () => { expect(entriesToObject(ruleConfigs)).toEqual(entriesToObject(configRules)); }); + + itHasBaseRulesOverriden(unfilteredConfigRules); }); diff --git a/packages/eslint-plugin/tests/docs.test.ts b/packages/eslint-plugin/tests/docs.test.ts index d85dc8fb204e..56a9e50002d1 100644 --- a/packages/eslint-plugin/tests/docs.test.ts +++ b/packages/eslint-plugin/tests/docs.test.ts @@ -11,7 +11,10 @@ function createRuleLink(ruleName: string): string { return `[\`@typescript-eslint/${ruleName}\`](./docs/rules/${ruleName}.md)`; } -function parseReadme(): marked.Tokens.Table { +function parseReadme(): { + base: marked.Tokens.Table; + extension: marked.Tokens.Table; +} { const readmeRaw = fs.readFileSync( path.resolve(__dirname, '../README.md'), 'utf8', @@ -22,14 +25,17 @@ function parseReadme(): marked.Tokens.Table { }); // find the table - const rulesTable = readme.find( + const rulesTables = readme.filter( (token): token is marked.Tokens.Table => token.type === 'table', ); - if (!rulesTable) { - throw Error('Could not find the rules table in README.md'); + if (rulesTables.length !== 2) { + throw Error('Could not find both rules tables in README.md'); } - return rulesTable; + return { + base: rulesTables[0], + extension: rulesTables[1], + }; } describe('Validating rule docs', () => { @@ -90,24 +96,47 @@ describe('Validating rule metadata', () => { }); describe('Validating README.md', () => { - const rulesTable = parseReadme().cells; - const notDeprecated = rulesData.filter( - ([, rule]) => rule.meta.deprecated !== true, + const rulesTables = parseReadme(); + const notDeprecated = rulesData.filter(([, rule]) => !rule.meta.deprecated); + const baseRules = notDeprecated.filter( + ([, rule]) => !rule.meta.docs.extendsBaseRule, ); + const extensionRules = notDeprecated.filter( + ([, rule]) => rule.meta.docs.extendsBaseRule, + ); + + it('All non-deprecated base rules should have a row in the base rules table, and the table should be ordered alphabetically', () => { + const baseRuleNames = baseRules + .map(([ruleName]) => ruleName) + .sort() + .map(createRuleLink); - it('All non-deprecated rules should have a row in the table, and the table should be ordered alphabetically', () => { - const ruleNames = notDeprecated + expect(rulesTables.base.cells.map(row => row[0])).toStrictEqual( + baseRuleNames, + ); + }); + it('All non-deprecated extension rules should have a row in the base rules table, and the table should be ordered alphabetically', () => { + const extensionRuleNames = extensionRules .map(([ruleName]) => ruleName) .sort() .map(createRuleLink); - expect(rulesTable.map(row => row[0])).toStrictEqual(ruleNames); + expect(rulesTables.extension.cells.map(row => row[0])).toStrictEqual( + extensionRuleNames, + ); }); for (const [ruleName, rule] of notDeprecated) { describe(`Checking rule ${ruleName}`, () => { - const ruleRow = - rulesTable.find(row => row[0].includes(`/${ruleName}.md`)) ?? []; + const ruleRow: string[] | undefined = (rule.meta.docs.extendsBaseRule + ? rulesTables.extension.cells + : rulesTables.base.cells + ).find(row => row[0].includes(`/${ruleName}.md`)); + if (!ruleRow) { + // rule is in the wrong table, the first two tests will catch this, so no point in creating noise; + // these tests will ofc fail in that case + return; + } it('Link column should be correct', () => { expect(ruleRow[0]).toEqual(createRuleLink(ruleName)); diff --git a/packages/eslint-plugin/tools/generate-configs.ts b/packages/eslint-plugin/tools/generate-configs.ts index d47d01bfdc0f..a8420756e48f 100644 --- a/packages/eslint-plugin/tools/generate-configs.ts +++ b/packages/eslint-plugin/tools/generate-configs.ts @@ -21,26 +21,19 @@ interface LinterConfig extends TSESLint.Linter.Config { const RULE_NAME_PREFIX = '@typescript-eslint/'; const MAX_RULE_NAME_LENGTH = 32; const DEFAULT_RULE_SETTING = 'warn'; -const BASE_RULES_TO_BE_OVERRIDDEN = new Set([ - 'brace-style', - 'camelcase', - 'default-param-last', - 'func-call-spacing', - 'indent', - 'no-array-constructor', - 'no-empty-function', - 'no-extra-parens', - 'no-extra-semi', - 'no-magic-numbers', - 'quotes', - 'no-unused-expressions', - 'no-unused-vars', - 'no-use-before-define', - 'no-useless-constructor', - 'require-await', - 'semi', - 'space-before-function-paren', -]); +const BASE_RULES_TO_BE_OVERRIDDEN = new Map( + Object.entries(rules) + .filter(([, rule]) => rule.meta.docs.extendsBaseRule) + .map( + ([ruleName, rule]) => + [ + ruleName, + typeof rule.meta.docs.extendsBaseRule === 'string' + ? rule.meta.docs.extendsBaseRule + : ruleName, + ] as const, + ), +); // list of rules from the base plugin that we think should be turned on for typescript code const BASE_RULES_THAT_ARE_RECOMMENDED = new Set([ 'no-var', @@ -100,14 +93,15 @@ function reducer( : recommendation; if (BASE_RULES_TO_BE_OVERRIDDEN.has(key)) { + const baseRuleName = BASE_RULES_TO_BE_OVERRIDDEN.get(key)!; console.log( - key - .padStart(RULE_NAME_PREFIX.length + key.length) + baseRuleName + .padStart(RULE_NAME_PREFIX.length + baseRuleName.length) .padEnd(RULE_NAME_PREFIX.length + MAX_RULE_NAME_LENGTH), '=', chalk.green('off'), ); - config[key] = 'off'; + config[baseRuleName] = 'off'; } console.log( `${chalk.dim(RULE_NAME_PREFIX)}${key.padEnd(MAX_RULE_NAME_LENGTH)}`, diff --git a/packages/experimental-utils/src/ts-eslint/Rule.ts b/packages/experimental-utils/src/ts-eslint/Rule.ts index da84f47a2b9d..845560e80372 100644 --- a/packages/experimental-utils/src/ts-eslint/Rule.ts +++ b/packages/experimental-utils/src/ts-eslint/Rule.ts @@ -33,6 +33,12 @@ interface RuleMetaDataDocs { * to type-check code. This is only used for documentation purposes. */ requiresTypeChecking?: boolean; + /** + * Does the rule extend (or is it based off of) an ESLint code rule? + * Alternately accepts the name of the base rule, in case the rule has been renamed. + * This is only used for documentation purposes. + */ + extendsBaseRule?: boolean | string; } interface RuleMetaData { /** From 58c7c25537485a5aa5b3398dd0400a2e08909b96 Mon Sep 17 00:00:00 2001 From: Alexander T Date: Thu, 16 Jan 2020 07:55:57 +0200 Subject: [PATCH 11/17] feat(eslint-plugin): [no-extra-!-assert] flag ?. after !-assert (#1460) --- .../docs/rules/no-extra-non-null-assertion.md | 12 +++++ .../src/rules/no-extra-non-null-assertion.ts | 12 +++-- .../rules/no-extra-non-null-assertion.test.ts | 49 +++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-extra-non-null-assertion.md b/packages/eslint-plugin/docs/rules/no-extra-non-null-assertion.md index ced5d83ccb7e..0991efdee52f 100644 --- a/packages/eslint-plugin/docs/rules/no-extra-non-null-assertion.md +++ b/packages/eslint-plugin/docs/rules/no-extra-non-null-assertion.md @@ -15,6 +15,12 @@ function foo(bar: number | undefined) { } ``` +```ts +function foo(bar?: { n: number }) { + return bar!?.n; +} +``` + Examples of **correct** code for this rule: ```ts @@ -28,6 +34,12 @@ function foo(bar: number | undefined) { } ``` +```ts +function foo(bar?: { n: number }) { + return bar?.n; +} +``` + ## How to use ```json diff --git a/packages/eslint-plugin/src/rules/no-extra-non-null-assertion.ts b/packages/eslint-plugin/src/rules/no-extra-non-null-assertion.ts index cd116a3a4d7b..39969de283f3 100644 --- a/packages/eslint-plugin/src/rules/no-extra-non-null-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-extra-non-null-assertion.ts @@ -1,4 +1,5 @@ import * as util from '../util'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; export default util.createRule({ name: 'no-extra-non-null-assertion', @@ -16,10 +17,15 @@ export default util.createRule({ }, defaultOptions: [], create(context) { + function checkExtraNonNullAssertion( + node: TSESTree.TSNonNullExpression, + ): void { + context.report({ messageId: 'noExtraNonNullAssertion', node }); + } + return { - 'TSNonNullExpression > TSNonNullExpression'(node): void { - context.report({ messageId: 'noExtraNonNullAssertion', node }); - }, + 'TSNonNullExpression > TSNonNullExpression': checkExtraNonNullAssertion, + 'OptionalMemberExpression > TSNonNullExpression': checkExtraNonNullAssertion, }; }, }); diff --git a/packages/eslint-plugin/tests/rules/no-extra-non-null-assertion.test.ts b/packages/eslint-plugin/tests/rules/no-extra-non-null-assertion.test.ts index a851fe9a0604..074dc90697ef 100644 --- a/packages/eslint-plugin/tests/rules/no-extra-non-null-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-extra-non-null-assertion.test.ts @@ -19,6 +19,13 @@ function foo(bar: number | undefined) { const bar: number = bar!; } `, }, + { + code: ` +function foo(bar?: { n: number }) { + return bar?.n; +} + `, + }, ], invalid: [ { @@ -62,5 +69,47 @@ function foo(bar: number | undefined) { }, ], }, + { + code: ` +function foo(bar?: { n: number }) { + return bar!?.n; +} + `, + errors: [ + { + messageId: 'noExtraNonNullAssertion', + endColumn: 14, + column: 10, + line: 3, + }, + ], + }, + { + code: ` +function foo(bar?: { n: number }) { + return bar!!!?.n; +} + `, + errors: [ + { + messageId: 'noExtraNonNullAssertion', + endColumn: 16, + column: 10, + line: 3, + }, + { + messageId: 'noExtraNonNullAssertion', + endColumn: 15, + column: 10, + line: 3, + }, + { + messageId: 'noExtraNonNullAssertion', + endColumn: 14, + column: 10, + line: 3, + }, + ], + }, ], }); From e3293978c512d0ebbaf6cf0701f26ef81f5276e3 Mon Sep 17 00:00:00 2001 From: Armano Date: Thu, 16 Jan 2020 17:31:45 +0100 Subject: [PATCH 12/17] test: update azure pipelines to run tests on node 12 (#1463) --- azure-pipelines.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8eca71b7c172..b80f0eea4b51 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -5,12 +5,12 @@ jobs: - job: primary_code_validation_and_tests displayName: Primary code validation and tests pool: - vmImage: 'Ubuntu-16.04' + vmImage: 'Ubuntu-18.04' steps: - task: NodeTool@0 inputs: - versionSpec: 11 - displayName: 'Install Node.js 11' + versionSpec: 12 + displayName: 'Install Node.js 12' - script: | # This also runs a build as part of the postinstall @@ -50,7 +50,7 @@ jobs: - job: unit_tests_on_other_node_versions displayName: Run unit tests on other Node.js versions pool: - vmImage: 'Ubuntu-16.04' + vmImage: 'Ubuntu-18.04' strategy: maxParallel: 3 matrix: @@ -80,12 +80,12 @@ jobs: - unit_tests_on_other_node_versions condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'master'), ne(variables['Build.Reason'], 'PullRequest')) pool: - vmImage: 'Ubuntu-16.04' + vmImage: 'Ubuntu-18.04' steps: - task: NodeTool@0 inputs: - versionSpec: 11 - displayName: 'Install Node.js 11' + versionSpec: 12 + displayName: 'Install Node.js 12' - script: | # This also runs a build as part of the postinstall From 9c5b857ccd5661a1ee56541d4f342b47d3f43595 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Thu, 16 Jan 2020 08:58:57 -0800 Subject: [PATCH 13/17] fix(eslint-plugin): [unbound-method] handling of logical expr (#1440) --- .../eslint-plugin/src/rules/unbound-method.ts | 21 +- .../tests/rules/unbound-method.test.ts | 368 +++++++----------- 2 files changed, 151 insertions(+), 238 deletions(-) diff --git a/packages/eslint-plugin/src/rules/unbound-method.ts b/packages/eslint-plugin/src/rules/unbound-method.ts index ac4dd6b28de3..32df694637e1 100644 --- a/packages/eslint-plugin/src/rules/unbound-method.ts +++ b/packages/eslint-plugin/src/rules/unbound-method.ts @@ -14,9 +14,9 @@ interface Config { ignoreStatic: boolean; } -type Options = [Config]; +export type Options = [Config]; -type MessageIds = 'unbound'; +export type MessageIds = 'unbound'; export default util.createRule({ name: 'unbound-method', @@ -99,9 +99,9 @@ function isDangerousMethod(symbol: ts.Symbol, ignoreStatic: boolean): boolean { } function isSafeUse(node: TSESTree.Node): boolean { - const parent = node.parent!; + const parent = node.parent; - switch (parent.type) { + switch (parent?.type) { case AST_NODE_TYPES.IfStatement: case AST_NODE_TYPES.ForStatement: case AST_NODE_TYPES.MemberExpression: @@ -118,9 +118,6 @@ function isSafeUse(node: TSESTree.Node): boolean { case AST_NODE_TYPES.ConditionalExpression: return parent.test === node; - case AST_NODE_TYPES.LogicalExpression: - return parent.operator !== '||'; - case AST_NODE_TYPES.TaggedTemplateExpression: return parent.tag === node; @@ -134,6 +131,16 @@ function isSafeUse(node: TSESTree.Node): boolean { case AST_NODE_TYPES.TSAsExpression: case AST_NODE_TYPES.TSTypeAssertion: return isSafeUse(parent); + + case AST_NODE_TYPES.LogicalExpression: + if (parent.operator === '&&' && parent.left === node) { + // this is safe, as && will return the left if and only if it's falsy + return true; + } + + // in all other cases, it's likely the logical expression will return the method ref + // so make sure the parent is a safe usage + return isSafeUse(parent); } return false; diff --git a/packages/eslint-plugin/tests/rules/unbound-method.test.ts b/packages/eslint-plugin/tests/rules/unbound-method.test.ts index 634977b477f2..6b5fd5500d46 100644 --- a/packages/eslint-plugin/tests/rules/unbound-method.test.ts +++ b/packages/eslint-plugin/tests/rules/unbound-method.test.ts @@ -1,5 +1,6 @@ +import { TSESLint } from '@typescript-eslint/experimental-utils'; import path from 'path'; -import rule from '../../src/rules/unbound-method'; +import rule, { MessageIds, Options } from '../../src/rules/unbound-method'; import { RuleTester } from '../RuleTester'; const rootPath = path.join(process.cwd(), 'tests/fixtures/'); @@ -12,9 +13,8 @@ const ruleTester = new RuleTester({ }, }); -ruleTester.run('unbound-method', rule, { - valid: [ - ` +function addContainsMethodsClass(code: string): string { + return ` class ContainsMethods { bound?: () => void; unbound?(): void; @@ -25,97 +25,125 @@ class ContainsMethods { let instance = new ContainsMethods(); -instance.bound(); -instance.unbound(); - -ContainsMethods.boundStatic(); -ContainsMethods.unboundStatic(); - -{ - const bound = instance.bound; - const boundStatic = ContainsMethods; +${code} + `; } -{ - const { bound } = instance; - const { boundStatic } = ContainsMethods; +function addContainsMethodsClassInvalid( + code: string[], +): TSESLint.InvalidTestCase[] { + return code.map(c => ({ + code: addContainsMethodsClass(c), + errors: [ + { + line: 12, + messageId: 'unbound', + }, + ], + })); } -(instance.bound)(); -(instance.unbound)(); +ruleTester.run('unbound-method', rule, { + valid: [ + ...[ + 'instance.bound();', + 'instance.unbound();', -(ContainsMethods.boundStatic)(); -(ContainsMethods.unboundStatic)(); + 'ContainsMethods.boundStatic();', + 'ContainsMethods.unboundStatic();', -instance.bound\`\`; -instance.unbound\`\`; + 'const bound = instance.bound;', + 'const boundStatic = ContainsMethods;', -if (instance.bound) { } -if (instance.unbound) { } + 'const { bound } = instance;', + 'const { boundStatic } = ContainsMethods;', -if (instance.bound !== undefined) { } -if (instance.unbound !== undefined) { } + '(instance.bound)();', + '(instance.unbound)();', -if (ContainsMethods.boundStatic) { } -if (ContainsMethods.unboundStatic) { } + '(ContainsMethods.boundStatic)();', + '(ContainsMethods.unboundStatic)();', -if (ContainsMethods.boundStatic !== undefined) { } -if (ContainsMethods.unboundStatic !== undefined) { } + 'instance.bound``;', + 'instance.unbound``;', -while (instance.bound) { } -while (instance.unbound) { } + 'if (instance.bound) { }', + 'if (instance.unbound) { }', -while (instance.bound !== undefined) { } -while (instance.unbound !== undefined) { } + 'if (instance.bound !== undefined) { }', + 'if (instance.unbound !== undefined) { }', -while (ContainsMethods.boundStatic) { } -while (ContainsMethods.unboundStatic) { } + 'if (ContainsMethods.boundStatic) { }', + 'if (ContainsMethods.unboundStatic) { }', -while (ContainsMethods.boundStatic !== undefined) { } -while (ContainsMethods.unboundStatic !== undefined) { } + 'if (ContainsMethods.boundStatic !== undefined) { }', + 'if (ContainsMethods.unboundStatic !== undefined) { }', -instance.bound as any; -ContainsMethods.boundStatic as any; + 'if (ContainsMethods.boundStatic && instance) { }', + 'if (ContainsMethods.unboundStatic && instance) { }', -instance.bound++; -+instance.bound; -++instance.bound; -instance.bound--; --instance.bound; ---instance.bound; -instance.bound += 1; -instance.bound -= 1; -instance.bound *= 1; -instance.bound /= 1; + 'if (instance.bound || instance) { }', + 'if (instance.unbound || instance) { }', -instance.bound || 0; -instane.bound && 0; + 'ContainsMethods.unboundStatic && 0 || ContainsMethods;', -instance.bound ? 1 : 0; -instance.unbound ? 1 : 0; + '(instance.bound || instance) ? 1 : 0', + '(instance.unbound || instance) ? 1 : 0', -ContainsMethods.boundStatic++; -+ContainsMethods.boundStatic; -++ContainsMethods.boundStatic; -ContainsMethods.boundStatic--; --ContainsMethods.boundStatic; ---ContainsMethods.boundStatic; -ContainsMethods.boundStatic += 1; -ContainsMethods.boundStatic -= 1; -ContainsMethods.boundStatic *= 1; -ContainsMethods.boundStatic /= 1; + 'while (instance.bound) { }', + 'while (instance.unbound) { }', -ContainsMethods.boundStatic || 0; -instane.boundStatic && 0; + 'while (instance.bound !== undefined) { }', + 'while (instance.unbound !== undefined) { }', -ContainsMethods.boundStatic ? 1 : 0; -ContainsMethods.unboundStatic ? 1 : 0; + 'while (ContainsMethods.boundStatic) { }', + 'while (ContainsMethods.unboundStatic) { }', -typeof instance.bound === 'function'; -typeof instance.unbound === 'function'; + 'while (ContainsMethods.boundStatic !== undefined) { }', + 'while (ContainsMethods.unboundStatic !== undefined) { }', -typeof ContainsMethods.boundStatic === 'function'; -typeof ContainsMethods.unboundStatic === 'function'; - `, + 'instance.bound as any;', + 'ContainsMethods.boundStatic as any;', + + 'instance.bound++;', + '+instance.bound;', + '++instance.bound;', + 'instance.bound--;', + '-instance.bound;', + '--instance.bound;', + 'instance.bound += 1;', + 'instance.bound -= 1;', + 'instance.bound *= 1;', + 'instance.bound /= 1;', + + 'instance.bound || 0;', + 'instance.bound && 0;', + + 'instance.bound ? 1 : 0;', + 'instance.unbound ? 1 : 0;', + + 'ContainsMethods.boundStatic++;', + '+ContainsMethods.boundStatic;', + '++ContainsMethods.boundStatic;', + 'ContainsMethods.boundStatic--;', + '-ContainsMethods.boundStatic;', + '--ContainsMethods.boundStatic;', + 'ContainsMethods.boundStatic += 1;', + 'ContainsMethods.boundStatic -= 1;', + 'ContainsMethods.boundStatic *= 1;', + 'ContainsMethods.boundStatic /= 1;', + + 'ContainsMethods.boundStatic || 0;', + 'instane.boundStatic && 0;', + + 'ContainsMethods.boundStatic ? 1 : 0;', + 'ContainsMethods.unboundStatic ? 1 : 0;', + + "typeof instance.bound === 'function';", + "typeof instance.unbound === 'function';", + + "typeof ContainsMethods.boundStatic === 'function';", + "typeof ContainsMethods.unboundStatic === 'function';", + ].map(addContainsMethodsClass), ` interface RecordA { readonly type: "A" @@ -165,185 +193,63 @@ function foo(instance: ContainsMethods | null) { typeof instance?.bound === 'function'; typeof instance?.unbound === 'function'; +} + `, + // https://github.com/typescript-eslint/typescript-eslint/issues/1425 + ` +interface OptionalMethod { + mightBeDefined?(): void +} + +const x: OptionalMethod = {}; +declare const myCondition: boolean; +if(myCondition || x.mightBeDefined) { + console.log('hello world') } `, ], invalid: [ { - code: ` -class ContainsMethods { - bound?: () => void; - unbound?(): void; - static boundStatic?: () => void; - static unboundStatic?(): void; -} - -function foo(instance: ContainsMethods | null) { - const unbound = instance?.unbound; - instance.unbound += 1; - instance?.unbound as any; + code: addContainsMethodsClass(` +function foo(arg: ContainsMethods | null) { + const unbound = arg?.unbound; + arg.unbound += 1; + arg?.unbound as any; } - `, + `), errors: [ { - line: 10, + line: 14, messageId: 'unbound', }, { - line: 11, + line: 15, messageId: 'unbound', }, { - line: 12, + line: 16, messageId: 'unbound', }, ], }, - { - code: ` -class ContainsMethods { - bound?: () => void; - unbound?(): void; - static boundStatic?: () => void; - static unboundStatic?(): void; -} + ...addContainsMethodsClassInvalid([ + 'const unbound = instance.unbound;', + 'const unboundStatic = ContainsMethods.unboundStatic;', -const instance = new ContainsMethods(); + 'const { unbound } = instance.unbound;', + 'const { unboundStatic } = ContainsMethods.unboundStatic;', -{ - const unbound = instance.unbound; - const unboundStatic = ContainsMethods.unboundStatic; -} -{ - const { unbound } = instance.unbound; - const { unboundStatic } = ContainsMethods.unboundStatic; -} + 'instance.unbound;', + 'instance.unbound as any;', -instance.unbound; -instance.unbound as any; - -ContainsMethods.unboundStatic; -ContainsMethods.unboundStatic as any; - -instance.unbound++; -+instance.unbound; -++instance.unbound; -instance.unbound--; --instance.unbound; ---instance.unbound; -instance.unbound += 1; -instance.unbound -= 1; -instance.unbound *= 1; -instance.unbound /= 1; - -instance.unbound || 0; -instance.unbound && 0; - -ContainsMethods.unboundStatic++; -+ContainsMethods.unboundStatic; -++ContainsMethods.unboundStatic; -ContainsMethods.unboundStatic--; --ContainsMethods.unboundStatic; ---ContainsMethods.unboundStatic; -ContainsMethods.unboundStatic += 1; -ContainsMethods.unboundStatic -= 1; -ContainsMethods.unboundStatic *= 1; -ContainsMethods.unboundStatic /= 1; - -ContainsMethods.unboundStatic || 0; -ContainsMethods.unboundStatic && 0; -`, - errors: [ - { - line: 12, - messageId: 'unbound', - }, - { - line: 13, - messageId: 'unbound', - }, - { - line: 16, - messageId: 'unbound', - }, - { - line: 17, - messageId: 'unbound', - }, - { - line: 20, - messageId: 'unbound', - }, - { - line: 21, - messageId: 'unbound', - }, - { - line: 23, - messageId: 'unbound', - }, - { - line: 24, - messageId: 'unbound', - }, - { - line: 27, - messageId: 'unbound', - }, - { - line: 30, - messageId: 'unbound', - }, - { - line: 32, - messageId: 'unbound', - }, - { - line: 33, - messageId: 'unbound', - }, - { - line: 34, - messageId: 'unbound', - }, - { - line: 35, - messageId: 'unbound', - }, - { - line: 37, - messageId: 'unbound', - }, - { - line: 41, - messageId: 'unbound', - }, - { - line: 44, - messageId: 'unbound', - }, - { - line: 46, - messageId: 'unbound', - }, - { - line: 47, - messageId: 'unbound', - }, - { - line: 48, - messageId: 'unbound', - }, - { - line: 49, - messageId: 'unbound', - }, - { - line: 51, - messageId: 'unbound', - }, - ], - }, + 'ContainsMethods.unboundStatic;', + 'ContainsMethods.unboundStatic as any;', + + 'instance.unbound || 0;', + 'ContainsMethods.unboundStatic || 0;', + + 'instance.unbound ? instance.unbound : null', + ]), { code: ` class ContainsMethods { From 498aa24383885e95c06830ef058ef8296666d1a9 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Fri, 17 Jan 2020 23:16:50 -0800 Subject: [PATCH 14/17] feat(eslint-plugin): add no-non-null-asserted-optional-chain (#1469) --- packages/eslint-plugin/README.md | 157 +++++++-------- .../no-non-null-asserted-optional-chain.md | 39 ++++ packages/eslint-plugin/src/configs/all.json | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../no-non-null-asserted-optional-chain.ts | 52 +++++ ...o-non-null-asserted-optional-chain.test.ts | 187 ++++++++++++++++++ .../eslint-plugin/tools/generate-configs.ts | 5 +- 7 files changed, 364 insertions(+), 79 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/no-non-null-asserted-optional-chain.md create mode 100644 packages/eslint-plugin/src/rules/no-non-null-asserted-optional-chain.ts create mode 100644 packages/eslint-plugin/tests/rules/no-non-null-asserted-optional-chain.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 2111de5cee39..f2418485d521 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -93,64 +93,65 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int **Key**: :heavy_check_mark: = recommended, :wrench: = fixable, :thought_balloon: = requires type information -| Name | Description | :heavy_check_mark: | :wrench: | :thought_balloon: | -| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -------- | ----------------- | -| [`@typescript-eslint/adjacent-overload-signatures`](./docs/rules/adjacent-overload-signatures.md) | Require that member overloads be consecutive | :heavy_check_mark: | | | -| [`@typescript-eslint/array-type`](./docs/rules/array-type.md) | Requires using either `T[]` or `Array` for arrays | | :wrench: | | -| [`@typescript-eslint/await-thenable`](./docs/rules/await-thenable.md) | Disallows awaiting a value that is not a Thenable | :heavy_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/ban-ts-ignore`](./docs/rules/ban-ts-ignore.md) | Bans “// @ts-ignore” comments from being used | :heavy_check_mark: | | | -| [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Bans specific types from being used | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforces consistent usage of type assertions | :heavy_check_mark: | | | -| [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Consistent with type definition either `interface` or `type` | | :wrench: | | -| [`@typescript-eslint/explicit-function-return-type`](./docs/rules/explicit-function-return-type.md) | Require explicit return types on functions and class methods | :heavy_check_mark: | | | -| [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | | | -| [`@typescript-eslint/explicit-module-boundary-types`](./docs/rules/explicit-module-boundary-types.md) | Require explicit return and argument types on exported functions' and classes' public class methods | | | | -| [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order | | | | -| [`@typescript-eslint/naming-convention`](./docs/rules/naming-convention.md) | Enforces naming conventions for everything across a codebase | | | :thought_balloon: | -| [`@typescript-eslint/no-dynamic-delete`](./docs/rules/no-dynamic-delete.md) | Disallow the delete operator with computed key expressions | | :wrench: | | -| [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/no-extra-non-null-assertion`](./docs/rules/no-extra-non-null-assertion.md) | Disallow extra non-null assertion | | | | -| [`@typescript-eslint/no-extraneous-class`](./docs/rules/no-extraneous-class.md) | Forbids the use of classes as namespaces | | | | -| [`@typescript-eslint/no-floating-promises`](./docs/rules/no-floating-promises.md) | Requires Promise-like values to be handled appropriately | | | :thought_balloon: | -| [`@typescript-eslint/no-for-in-array`](./docs/rules/no-for-in-array.md) | Disallow iterating over an array with a for-in loop | :heavy_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-implied-eval`](./docs/rules/no-implied-eval.md) | Disallow the use of `eval()`-like methods | | | :thought_balloon:| -| [`@typescript-eslint/no-inferrable-types`](./docs/rules/no-inferrable-types.md) | Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/no-misused-new`](./docs/rules/no-misused-new.md) | Enforce valid definition of `new` and `constructor` | :heavy_check_mark: | | | -| [`@typescript-eslint/no-misused-promises`](./docs/rules/no-misused-promises.md) | Avoid using promises in places not designed to handle them | :heavy_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-namespace`](./docs/rules/no-namespace.md) | Disallow the use of custom TypeScript modules and namespaces | :heavy_check_mark: | | | -| [`@typescript-eslint/no-non-null-assertion`](./docs/rules/no-non-null-assertion.md) | Disallows non-null assertions using the `!` postfix operator | :heavy_check_mark: | | | -| [`@typescript-eslint/no-parameter-properties`](./docs/rules/no-parameter-properties.md) | Disallow the use of parameter properties in class constructors | | | | -| [`@typescript-eslint/no-require-imports`](./docs/rules/no-require-imports.md) | Disallows invocation of `require()` | | | | -| [`@typescript-eslint/no-this-alias`](./docs/rules/no-this-alias.md) | Disallow aliasing `this` | :heavy_check_mark: | | | -| [`@typescript-eslint/no-throw-literal`](./docs/rules/no-throw-literal.md) | Disallow throwing literals as exceptions | | | :thought_balloon: | -| [`@typescript-eslint/no-type-alias`](./docs/rules/no-type-alias.md) | Disallow the use of type aliases | | | | -| [`@typescript-eslint/no-unnecessary-condition`](./docs/rules/no-unnecessary-condition.md) | Prevents conditionals where the type is always truthy or always falsy | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-unnecessary-qualifier`](./docs/rules/no-unnecessary-qualifier.md) | Warns when a namespace qualifier is unnecessary | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-unnecessary-type-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Enforces that type arguments will not be used if not required | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | :heavy_check_mark: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments | | | :thought_balloon: | -| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | | -| [`@typescript-eslint/prefer-for-of`](./docs/rules/prefer-for-of.md) | Prefer a ‘for-of’ loop over a standard ‘for’ loop if the index is only used to access the array being iterated | | | | -| [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures | | :wrench: | | -| [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method | :heavy_check_mark: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require the use of the `namespace` keyword instead of the `module` keyword to declare custom TypeScript modules | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/prefer-nullish-coalescing`](./docs/rules/prefer-nullish-coalescing.md) | Enforce the usage of the nullish coalescing operator instead of logical chaining | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-optional-chain`](./docs/rules/prefer-optional-chain.md) | Prefer using concise optional chain expressions instead of chained logical ands | | :wrench: | | -| [`@typescript-eslint/prefer-readonly`](./docs/rules/prefer-readonly.md) | Requires that private members are marked as `readonly` if they're never modified outside of the constructor | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | :heavy_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/prefer-string-starts-ends-with`](./docs/rules/prefer-string-starts-ends-with.md) | Enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings | :heavy_check_mark: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async | | | :thought_balloon: | -| [`@typescript-eslint/require-array-sort-compare`](./docs/rules/require-array-sort-compare.md) | Requires `Array#sort` calls to always provide a `compareFunction` | | | :thought_balloon: | -| [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string | | | :thought_balloon: | -| [`@typescript-eslint/restrict-template-expressions`](./docs/rules/restrict-template-expressions.md) | Enforce template literal expressions to be of string type | | | :thought_balloon: | -| [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: | -| [`@typescript-eslint/triple-slash-reference`](./docs/rules/triple-slash-reference.md) | Sets preference level for triple slash directives versus ES6-style import declarations | :heavy_check_mark: | | | -| [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/typedef`](./docs/rules/typedef.md) | Requires type annotations to exist | | | | -| [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforces unbound methods are called with their expected scope | :heavy_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter | | | | +| Name | Description | :heavy_check_mark: | :wrench: | :thought_balloon: | +| --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -------- | ----------------- | +| [`@typescript-eslint/adjacent-overload-signatures`](./docs/rules/adjacent-overload-signatures.md) | Require that member overloads be consecutive | :heavy_check_mark: | | | +| [`@typescript-eslint/array-type`](./docs/rules/array-type.md) | Requires using either `T[]` or `Array` for arrays | | :wrench: | | +| [`@typescript-eslint/await-thenable`](./docs/rules/await-thenable.md) | Disallows awaiting a value that is not a Thenable | :heavy_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/ban-ts-ignore`](./docs/rules/ban-ts-ignore.md) | Bans “// @ts-ignore” comments from being used | :heavy_check_mark: | | | +| [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Bans specific types from being used | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforces consistent usage of type assertions | :heavy_check_mark: | | | +| [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Consistent with type definition either `interface` or `type` | | :wrench: | | +| [`@typescript-eslint/explicit-function-return-type`](./docs/rules/explicit-function-return-type.md) | Require explicit return types on functions and class methods | :heavy_check_mark: | | | +| [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | | | +| [`@typescript-eslint/explicit-module-boundary-types`](./docs/rules/explicit-module-boundary-types.md) | Require explicit return and argument types on exported functions' and classes' public class methods | | | | +| [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order | | | | +| [`@typescript-eslint/naming-convention`](./docs/rules/naming-convention.md) | Enforces naming conventions for everything across a codebase | | | :thought_balloon: | +| [`@typescript-eslint/no-dynamic-delete`](./docs/rules/no-dynamic-delete.md) | Disallow the delete operator with computed key expressions | | :wrench: | | +| [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/no-extra-non-null-assertion`](./docs/rules/no-extra-non-null-assertion.md) | Disallow extra non-null assertion | | | | +| [`@typescript-eslint/no-extraneous-class`](./docs/rules/no-extraneous-class.md) | Forbids the use of classes as namespaces | | | | +| [`@typescript-eslint/no-floating-promises`](./docs/rules/no-floating-promises.md) | Requires Promise-like values to be handled appropriately | | | :thought_balloon: | +| [`@typescript-eslint/no-for-in-array`](./docs/rules/no-for-in-array.md) | Disallow iterating over an array with a for-in loop | :heavy_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-implied-eval`](./docs/rules/no-implied-eval.md) | Disallow the use of `eval()`-like methods | | | :thought_balloon: | +| [`@typescript-eslint/no-inferrable-types`](./docs/rules/no-inferrable-types.md) | Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/no-misused-new`](./docs/rules/no-misused-new.md) | Enforce valid definition of `new` and `constructor` | :heavy_check_mark: | | | +| [`@typescript-eslint/no-misused-promises`](./docs/rules/no-misused-promises.md) | Avoid using promises in places not designed to handle them | :heavy_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-namespace`](./docs/rules/no-namespace.md) | Disallow the use of custom TypeScript modules and namespaces | :heavy_check_mark: | | | +| [`@typescript-eslint/no-non-null-asserted-optional-chain`](./docs/rules/no-non-null-asserted-optional-chain.md) | Disallows using a non-null assertion after an optional chain expression | | | | +| [`@typescript-eslint/no-non-null-assertion`](./docs/rules/no-non-null-assertion.md) | Disallows non-null assertions using the `!` postfix operator | :heavy_check_mark: | | | +| [`@typescript-eslint/no-parameter-properties`](./docs/rules/no-parameter-properties.md) | Disallow the use of parameter properties in class constructors | | | | +| [`@typescript-eslint/no-require-imports`](./docs/rules/no-require-imports.md) | Disallows invocation of `require()` | | | | +| [`@typescript-eslint/no-this-alias`](./docs/rules/no-this-alias.md) | Disallow aliasing `this` | :heavy_check_mark: | | | +| [`@typescript-eslint/no-throw-literal`](./docs/rules/no-throw-literal.md) | Disallow throwing literals as exceptions | | | :thought_balloon: | +| [`@typescript-eslint/no-type-alias`](./docs/rules/no-type-alias.md) | Disallow the use of type aliases | | | | +| [`@typescript-eslint/no-unnecessary-condition`](./docs/rules/no-unnecessary-condition.md) | Prevents conditionals where the type is always truthy or always falsy | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unnecessary-qualifier`](./docs/rules/no-unnecessary-qualifier.md) | Warns when a namespace qualifier is unnecessary | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unnecessary-type-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Enforces that type arguments will not be used if not required | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | :heavy_check_mark: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments | | | :thought_balloon: | +| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | | +| [`@typescript-eslint/prefer-for-of`](./docs/rules/prefer-for-of.md) | Prefer a ‘for-of’ loop over a standard ‘for’ loop if the index is only used to access the array being iterated | | | | +| [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures | | :wrench: | | +| [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method | :heavy_check_mark: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require the use of the `namespace` keyword instead of the `module` keyword to declare custom TypeScript modules | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/prefer-nullish-coalescing`](./docs/rules/prefer-nullish-coalescing.md) | Enforce the usage of the nullish coalescing operator instead of logical chaining | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-optional-chain`](./docs/rules/prefer-optional-chain.md) | Prefer using concise optional chain expressions instead of chained logical ands | | :wrench: | | +| [`@typescript-eslint/prefer-readonly`](./docs/rules/prefer-readonly.md) | Requires that private members are marked as `readonly` if they're never modified outside of the constructor | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | :heavy_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/prefer-string-starts-ends-with`](./docs/rules/prefer-string-starts-ends-with.md) | Enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings | :heavy_check_mark: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async | | | :thought_balloon: | +| [`@typescript-eslint/require-array-sort-compare`](./docs/rules/require-array-sort-compare.md) | Requires `Array#sort` calls to always provide a `compareFunction` | | | :thought_balloon: | +| [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string | | | :thought_balloon: | +| [`@typescript-eslint/restrict-template-expressions`](./docs/rules/restrict-template-expressions.md) | Enforce template literal expressions to be of string type | | | :thought_balloon: | +| [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: | +| [`@typescript-eslint/triple-slash-reference`](./docs/rules/triple-slash-reference.md) | Sets preference level for triple slash directives versus ES6-style import declarations | :heavy_check_mark: | | | +| [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/typedef`](./docs/rules/typedef.md) | Requires type annotations to exist | | | | +| [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforces unbound methods are called with their expected scope | :heavy_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter | | | | @@ -164,26 +165,26 @@ In these cases, we create what we call an extension rule; a rule within our plug **Key**: :heavy_check_mark: = recommended, :wrench: = fixable, :thought_balloon: = requires type information -| Name | Description | :heavy_check_mark: | :wrench: | :thought_balloon: | -| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -------- | ----------------- | -| [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | | -| [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | | -| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | | -| [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | | -| [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | | -| [`@typescript-eslint/no-extra-parens`](./docs/rules/no-extra-parens.md) | Disallow unnecessary parentheses | | :wrench: | | -| [`@typescript-eslint/no-extra-semi`](./docs/rules/no-extra-semi.md) | Disallow unnecessary semicolons | | :wrench: | | -| [`@typescript-eslint/no-magic-numbers`](./docs/rules/no-magic-numbers.md) | Disallow magic numbers | | | | -| [`@typescript-eslint/no-unused-expressions`](./docs/rules/no-unused-expressions.md) | Disallow unused expressions | | | | -| [`@typescript-eslint/no-unused-vars`](./docs/rules/no-unused-vars.md) | Disallow unused variables | :heavy_check_mark: | | | -| [`@typescript-eslint/no-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | :heavy_check_mark: | | | -| [`@typescript-eslint/no-useless-constructor`](./docs/rules/no-useless-constructor.md) | Disallow unnecessary constructors | | | | -| [`@typescript-eslint/quotes`](./docs/rules/quotes.md) | Enforce the consistent use of either backticks, double, or single quotes | | :wrench: | | -| [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :heavy_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/return-await`](./docs/rules/return-await.md) | Enforces consistent returning of awaited values | | | :thought_balloon: | -| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | | -| [`@typescript-eslint/space-before-function-paren`](./docs/rules/space-before-function-paren.md) | Enforces consistent spacing before function parenthesis | | :wrench: | | +| Name | Description | :heavy_check_mark: | :wrench: | :thought_balloon: | +| --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -------- | ----------------- | +| [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | | +| [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | | +| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | | +| [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | | +| [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | | +| [`@typescript-eslint/no-extra-parens`](./docs/rules/no-extra-parens.md) | Disallow unnecessary parentheses | | :wrench: | | +| [`@typescript-eslint/no-extra-semi`](./docs/rules/no-extra-semi.md) | Disallow unnecessary semicolons | | :wrench: | | +| [`@typescript-eslint/no-magic-numbers`](./docs/rules/no-magic-numbers.md) | Disallow magic numbers | | | | +| [`@typescript-eslint/no-unused-expressions`](./docs/rules/no-unused-expressions.md) | Disallow unused expressions | | | | +| [`@typescript-eslint/no-unused-vars`](./docs/rules/no-unused-vars.md) | Disallow unused variables | :heavy_check_mark: | | | +| [`@typescript-eslint/no-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | :heavy_check_mark: | | | +| [`@typescript-eslint/no-useless-constructor`](./docs/rules/no-useless-constructor.md) | Disallow unnecessary constructors | | | | +| [`@typescript-eslint/quotes`](./docs/rules/quotes.md) | Enforce the consistent use of either backticks, double, or single quotes | | :wrench: | | +| [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :heavy_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/return-await`](./docs/rules/return-await.md) | Enforces consistent returning of awaited values | | | :thought_balloon: | +| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | | +| [`@typescript-eslint/space-before-function-paren`](./docs/rules/space-before-function-paren.md) | Enforces consistent spacing before function parenthesis | | :wrench: | | diff --git a/packages/eslint-plugin/docs/rules/no-non-null-asserted-optional-chain.md b/packages/eslint-plugin/docs/rules/no-non-null-asserted-optional-chain.md new file mode 100644 index 000000000000..95ee927b22e2 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-non-null-asserted-optional-chain.md @@ -0,0 +1,39 @@ +# Disallows using a non-null assertion after an optional chain expression (`no-non-null-asserted-optional-chain`) + +## Rule Details + +Optional chain expressions are designed to return `undefined` if the optional property is nullish. +Using non-null assertions after an optional chain expression is wrong, and introduces a serious type safety hole into your code. + +Examples of **incorrect** code for this rule: + +```ts +/* eslint @typescript-eslint/no-non-null-asserted-optional-chain: "error" */ + +foo?.bar!; +foo?.bar!.baz; +foo?.bar()!; +foo?.bar!(); +foo?.bar!().baz; +``` + +Examples of **correct** code for this rule: + +```ts +/* eslint @typescript-eslint/no-non-null-asserted-optional-chain: "error" */ + +foo?.bar; +(foo?.bar).baz; +foo?.bar(); +foo?.bar(); +foo?.bar().baz; +``` + +## When Not To Use It + +If you are not using TypeScript 3.7 (or greater), then you will not need to use this rule, as the operator is not supported. + +## Further Reading + +- [TypeScript 3.7 Release Notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html) +- [Optional Chaining Proposal](https://github.com/tc39/proposal-optional-chaining/) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 9f1f3c1816ee..91380db19320 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -44,6 +44,7 @@ "@typescript-eslint/no-misused-new": "error", "@typescript-eslint/no-misused-promises": "error", "@typescript-eslint/no-namespace": "error", + "@typescript-eslint/no-non-null-asserted-optional-chain": "error", "@typescript-eslint/no-non-null-assertion": "error", "@typescript-eslint/no-parameter-properties": "error", "@typescript-eslint/no-require-imports": "error", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 1caa90bb46ef..ac12f27e8ba3 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -38,6 +38,7 @@ import noMisusedNew from './no-misused-new'; import noMisusedPromises from './no-misused-promises'; import noNamespace from './no-namespace'; import noNonNullAssertion from './no-non-null-assertion'; +import noNonNullAssertedOptionalChain from './no-non-null-asserted-optional-chain'; import noParameterProperties from './no-parameter-properties'; import noRequireImports from './no-require-imports'; import noThisAlias from './no-this-alias'; @@ -120,6 +121,7 @@ export default { 'no-misused-promises': noMisusedPromises, 'no-namespace': noNamespace, 'no-non-null-assertion': noNonNullAssertion, + 'no-non-null-asserted-optional-chain': noNonNullAssertedOptionalChain, 'no-parameter-properties': noParameterProperties, 'no-require-imports': noRequireImports, 'no-this-alias': noThisAlias, diff --git a/packages/eslint-plugin/src/rules/no-non-null-asserted-optional-chain.ts b/packages/eslint-plugin/src/rules/no-non-null-asserted-optional-chain.ts new file mode 100644 index 000000000000..2487c975df48 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-non-null-asserted-optional-chain.ts @@ -0,0 +1,52 @@ +import { TSESTree, TSESLint } from '@typescript-eslint/experimental-utils'; +import * as util from '../util'; + +type MessageIds = 'noNonNullOptionalChain' | 'suggestRemovingNonNull'; + +export default util.createRule<[], MessageIds>({ + name: 'no-non-null-asserted-optional-chain', + meta: { + type: 'problem', + docs: { + description: + 'Disallows using a non-null assertion after an optional chain expression', + category: 'Possible Errors', + recommended: false, + }, + messages: { + noNonNullOptionalChain: + 'Optional chain expressions can return undefined by design - using a non-null assertion is unsafe and wrong.', + suggestRemovingNonNull: 'You should remove the non-null assertion.', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + return { + 'TSNonNullExpression > :matches(OptionalMemberExpression, OptionalCallExpression)'( + node: + | TSESTree.OptionalCallExpression + | TSESTree.OptionalMemberExpression, + ): void { + // selector guarantees this assertion + const parent = node.parent as TSESTree.TSNonNullExpression; + context.report({ + node, + messageId: 'noNonNullOptionalChain', + // use a suggestion instead of a fixer, because this can obviously break type checks + suggest: [ + { + messageId: 'suggestRemovingNonNull', + fix(fixer): TSESLint.RuleFix { + return fixer.removeRange([ + parent.range[1] - 1, + parent.range[1], + ]); + }, + }, + ], + }); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-non-null-asserted-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/no-non-null-asserted-optional-chain.test.ts new file mode 100644 index 000000000000..33e95fa91a99 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-non-null-asserted-optional-chain.test.ts @@ -0,0 +1,187 @@ +import rule from '../../src/rules/no-non-null-asserted-optional-chain'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-non-null-asserted-optional-chain', rule, { + valid: [ + 'foo.bar!', + 'foo.bar()!', + 'foo?.bar', + 'foo?.bar()', + '(foo?.bar).baz!', + '(foo?.bar()).baz!', + ], + invalid: [ + { + code: 'foo?.bar!', + errors: [ + { + messageId: 'noNonNullOptionalChain', + suggestions: [ + { + messageId: 'suggestRemovingNonNull', + output: 'foo?.bar', + }, + ], + }, + ], + }, + { + code: 'foo?.["bar"]!', + errors: [ + { + messageId: 'noNonNullOptionalChain', + suggestions: [ + { + messageId: 'suggestRemovingNonNull', + output: 'foo?.["bar"]', + }, + ], + }, + ], + }, + { + code: 'foo?.bar()!', + errors: [ + { + messageId: 'noNonNullOptionalChain', + suggestions: [ + { + messageId: 'suggestRemovingNonNull', + output: 'foo?.bar()', + }, + ], + }, + ], + }, + { + code: 'foo.bar?.()!', + errors: [ + { + messageId: 'noNonNullOptionalChain', + suggestions: [ + { + messageId: 'suggestRemovingNonNull', + output: 'foo.bar?.()', + }, + ], + }, + ], + }, + { + code: 'foo?.bar!()', + errors: [ + { + messageId: 'noNonNullOptionalChain', + suggestions: [ + { + messageId: 'suggestRemovingNonNull', + output: 'foo?.bar()', + }, + ], + }, + ], + }, + { + code: '(foo?.bar)!.baz', + errors: [ + { + messageId: 'noNonNullOptionalChain', + suggestions: [ + { + messageId: 'suggestRemovingNonNull', + output: '(foo?.bar).baz', + }, + ], + }, + ], + }, + { + code: 'foo?.["bar"]!.baz', + errors: [ + { + messageId: 'noNonNullOptionalChain', + suggestions: [ + { + messageId: 'suggestRemovingNonNull', + output: 'foo?.["bar"].baz', + }, + ], + }, + ], + }, + { + code: '(foo?.bar)!().baz', + errors: [ + { + messageId: 'noNonNullOptionalChain', + suggestions: [ + { + messageId: 'suggestRemovingNonNull', + output: '(foo?.bar)().baz', + }, + ], + }, + ], + }, + { + code: '(foo?.bar)!', + errors: [ + { + messageId: 'noNonNullOptionalChain', + suggestions: [ + { + messageId: 'suggestRemovingNonNull', + output: '(foo?.bar)', + }, + ], + }, + ], + }, + { + code: '(foo?.bar)!()', + errors: [ + { + messageId: 'noNonNullOptionalChain', + suggestions: [ + { + messageId: 'suggestRemovingNonNull', + output: '(foo?.bar)()', + }, + ], + }, + ], + }, + { + code: '(foo?.bar!)', + errors: [ + { + messageId: 'noNonNullOptionalChain', + suggestions: [ + { + messageId: 'suggestRemovingNonNull', + output: '(foo?.bar)', + }, + ], + }, + ], + }, + { + code: '(foo?.bar!)()', + errors: [ + { + messageId: 'noNonNullOptionalChain', + suggestions: [ + { + messageId: 'suggestRemovingNonNull', + output: '(foo?.bar)()', + }, + ], + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tools/generate-configs.ts b/packages/eslint-plugin/tools/generate-configs.ts index a8420756e48f..33ac44c65caa 100644 --- a/packages/eslint-plugin/tools/generate-configs.ts +++ b/packages/eslint-plugin/tools/generate-configs.ts @@ -19,7 +19,10 @@ interface LinterConfig extends TSESLint.Linter.Config { } const RULE_NAME_PREFIX = '@typescript-eslint/'; -const MAX_RULE_NAME_LENGTH = 32; +const MAX_RULE_NAME_LENGTH = Object.keys(rules).reduce( + (acc, name) => Math.max(acc, name.length), + 0, +); const DEFAULT_RULE_SETTING = 'warn'; const BASE_RULES_TO_BE_OVERRIDDEN = new Map( Object.entries(rules) From 63c63517ada7013b141f82e1fa14927dbf66f231 Mon Sep 17 00:00:00 2001 From: Susisu Date: Mon, 20 Jan 2020 03:17:04 +0900 Subject: [PATCH 15/17] docs(eslint-plugin): example in [default-param-last] (#1471) --- packages/eslint-plugin/docs/rules/default-param-last.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/default-param-last.md b/packages/eslint-plugin/docs/rules/default-param-last.md index 8f2518c50ae5..202723907137 100644 --- a/packages/eslint-plugin/docs/rules/default-param-last.md +++ b/packages/eslint-plugin/docs/rules/default-param-last.md @@ -10,7 +10,6 @@ Examples of **incorrect** code for this rule: /* eslint @typescript-eslint/default-param-last: ["error"] */ function f(a = 0, b: number) {} -function f(a: number, b = 0, c?: number) {} function f(a: number, b = 0, c: number) {} function f(a: number, b?: number, c: number) {} ``` @@ -24,6 +23,7 @@ function f(a = 0) {} function f(a: number, b = 0) {} function f(a: number, b?: number) {} function f(a: number, b?: number, c = 0) {} +function f(a: number, b = 0, c?: number) {} ``` Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/default-param-last.md) From 60683d7a670d1ec261c7e683f83d9b09202cc879 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Mon, 20 Jan 2020 12:26:03 +1300 Subject: [PATCH 16/17] feat(eslint-plugin): [naming-convention] correct example (#1455) --- packages/eslint-plugin/docs/rules/naming-convention.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index 5ede1186e99b..df0cdb02e597 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -250,7 +250,7 @@ Group Selectors are provided for convenience, and essentially bundle up sets of "error", { "selector": "memberLike", - "modifier": ["private"], + "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require" } From 7641176a723be8d4cc030bacbb8ef6aa36abc3a5 Mon Sep 17 00:00:00 2001 From: James Henry Date: Mon, 20 Jan 2020 18:01:34 +0000 Subject: [PATCH 17/17] chore: publish v2.17.0 --- CHANGELOG.md | 24 ++++++++++++++++++++ lerna.json | 2 +- packages/eslint-plugin-internal/CHANGELOG.md | 8 +++++++ packages/eslint-plugin-internal/package.json | 4 ++-- packages/eslint-plugin-tslint/CHANGELOG.md | 11 +++++++++ packages/eslint-plugin-tslint/package.json | 6 ++--- packages/eslint-plugin/CHANGELOG.md | 24 ++++++++++++++++++++ packages/eslint-plugin/package.json | 4 ++-- packages/experimental-utils/CHANGELOG.md | 11 +++++++++ packages/experimental-utils/package.json | 4 ++-- packages/parser/CHANGELOG.md | 8 +++++++ packages/parser/package.json | 8 +++---- packages/shared-fixtures/CHANGELOG.md | 8 +++++++ packages/shared-fixtures/package.json | 2 +- packages/typescript-estree/CHANGELOG.md | 11 +++++++++ packages/typescript-estree/package.json | 4 ++-- 16 files changed, 122 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b933e712e88..09fb229a9173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,30 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.16.0...v2.17.0) (2020-01-20) + + +### Bug Fixes + +* **eslint-plugin:** [naming-convention] handle empty array-pattern ([#1450](https://github.com/typescript-eslint/typescript-eslint/issues/1450)) ([4726605](https://github.com/typescript-eslint/typescript-eslint/commit/4726605)) +* **eslint-plugin:** [unbound-method] handling of logical expr ([#1440](https://github.com/typescript-eslint/typescript-eslint/issues/1440)) ([9c5b857](https://github.com/typescript-eslint/typescript-eslint/commit/9c5b857)) +* **eslint-plugin:** set default-param-last as an extension rule ([#1445](https://github.com/typescript-eslint/typescript-eslint/issues/1445)) ([b5ef704](https://github.com/typescript-eslint/typescript-eslint/commit/b5ef704)) +* **typescript-estree:** correct type of `ArrayPattern.elements` ([#1451](https://github.com/typescript-eslint/typescript-eslint/issues/1451)) ([62e4ca0](https://github.com/typescript-eslint/typescript-eslint/commit/62e4ca0)) + + +### Features + +* **eslint-plugin:** [naming-convention] allow not check format ([#1455](https://github.com/typescript-eslint/typescript-eslint/issues/1455)) ([61eb434](https://github.com/typescript-eslint/typescript-eslint/commit/61eb434)) +* **eslint-plugin:** [naming-convention] correct example ([#1455](https://github.com/typescript-eslint/typescript-eslint/issues/1455)) ([60683d7](https://github.com/typescript-eslint/typescript-eslint/commit/60683d7)) +* **eslint-plugin:** [no-extra-!-assert] flag ?. after !-assert ([#1460](https://github.com/typescript-eslint/typescript-eslint/issues/1460)) ([58c7c25](https://github.com/typescript-eslint/typescript-eslint/commit/58c7c25)) +* **eslint-plugin:** add explicit-module-boundary-types rule ([#1020](https://github.com/typescript-eslint/typescript-eslint/issues/1020)) ([bb0a846](https://github.com/typescript-eslint/typescript-eslint/commit/bb0a846)) +* **eslint-plugin:** add no-non-null-asserted-optional-chain ([#1469](https://github.com/typescript-eslint/typescript-eslint/issues/1469)) ([498aa24](https://github.com/typescript-eslint/typescript-eslint/commit/498aa24)) +* **experimental-utils:** expose getParserServices from utils ([#1448](https://github.com/typescript-eslint/typescript-eslint/issues/1448)) ([982c8bc](https://github.com/typescript-eslint/typescript-eslint/commit/982c8bc)) + + + + + # [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) diff --git a/lerna.json b/lerna.json index 4e1f468786f5..ebaba89eec92 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.16.0", + "version": "2.17.0", "npmClient": "yarn", "useWorkspaces": true, "stream": true diff --git a/packages/eslint-plugin-internal/CHANGELOG.md b/packages/eslint-plugin-internal/CHANGELOG.md index 0ea39452d95f..748b630f4dca 100644 --- a/packages/eslint-plugin-internal/CHANGELOG.md +++ b/packages/eslint-plugin-internal/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.16.0...v2.17.0) (2020-01-20) + +**Note:** Version bump only for package @typescript-eslint/eslint-plugin-internal + + + + + # [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) **Note:** Version bump only for package @typescript-eslint/eslint-plugin-internal diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 603b3f244e35..de73fd89a286 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin-internal", - "version": "2.16.0", + "version": "2.17.0", "private": true, "main": "dist/index.js", "scripts": { @@ -12,6 +12,6 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "2.16.0" + "@typescript-eslint/experimental-utils": "2.17.0" } } diff --git a/packages/eslint-plugin-tslint/CHANGELOG.md b/packages/eslint-plugin-tslint/CHANGELOG.md index 42e6d600e4fb..ad86e8b5a379 100644 --- a/packages/eslint-plugin-tslint/CHANGELOG.md +++ b/packages/eslint-plugin-tslint/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.16.0...v2.17.0) (2020-01-20) + + +### Features + +* **experimental-utils:** expose getParserServices from utils ([#1448](https://github.com/typescript-eslint/typescript-eslint/issues/1448)) ([982c8bc](https://github.com/typescript-eslint/typescript-eslint/commit/982c8bc)) + + + + + # [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) **Note:** Version bump only for package @typescript-eslint/eslint-plugin-tslint diff --git a/packages/eslint-plugin-tslint/package.json b/packages/eslint-plugin-tslint/package.json index c9933a31e8c1..471a6c118ea8 100644 --- a/packages/eslint-plugin-tslint/package.json +++ b/packages/eslint-plugin-tslint/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin-tslint", - "version": "2.16.0", + "version": "2.17.0", "main": "dist/index.js", "typings": "src/index.ts", "description": "TSLint wrapper plugin for ESLint", @@ -31,7 +31,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "2.16.0", + "@typescript-eslint/experimental-utils": "2.17.0", "lodash": "^4.17.15" }, "peerDependencies": { @@ -41,6 +41,6 @@ }, "devDependencies": { "@types/lodash": "^4.14.149", - "@typescript-eslint/parser": "2.16.0" + "@typescript-eslint/parser": "2.17.0" } } diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index ea96304a87d3..34568bf986bb 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -3,6 +3,30 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.16.0...v2.17.0) (2020-01-20) + + +### Bug Fixes + +* **eslint-plugin:** [naming-convention] handle empty array-pattern ([#1450](https://github.com/typescript-eslint/typescript-eslint/issues/1450)) ([4726605](https://github.com/typescript-eslint/typescript-eslint/commit/4726605)) +* **eslint-plugin:** [unbound-method] handling of logical expr ([#1440](https://github.com/typescript-eslint/typescript-eslint/issues/1440)) ([9c5b857](https://github.com/typescript-eslint/typescript-eslint/commit/9c5b857)) +* **eslint-plugin:** set default-param-last as an extension rule ([#1445](https://github.com/typescript-eslint/typescript-eslint/issues/1445)) ([b5ef704](https://github.com/typescript-eslint/typescript-eslint/commit/b5ef704)) +* **typescript-estree:** correct type of `ArrayPattern.elements` ([#1451](https://github.com/typescript-eslint/typescript-eslint/issues/1451)) ([62e4ca0](https://github.com/typescript-eslint/typescript-eslint/commit/62e4ca0)) + + +### Features + +* **eslint-plugin:** [naming-convention] allow not check format ([#1455](https://github.com/typescript-eslint/typescript-eslint/issues/1455)) ([61eb434](https://github.com/typescript-eslint/typescript-eslint/commit/61eb434)) +* **eslint-plugin:** [naming-convention] correct example ([#1455](https://github.com/typescript-eslint/typescript-eslint/issues/1455)) ([60683d7](https://github.com/typescript-eslint/typescript-eslint/commit/60683d7)) +* **eslint-plugin:** [no-extra-!-assert] flag ?. after !-assert ([#1460](https://github.com/typescript-eslint/typescript-eslint/issues/1460)) ([58c7c25](https://github.com/typescript-eslint/typescript-eslint/commit/58c7c25)) +* **eslint-plugin:** add explicit-module-boundary-types rule ([#1020](https://github.com/typescript-eslint/typescript-eslint/issues/1020)) ([bb0a846](https://github.com/typescript-eslint/typescript-eslint/commit/bb0a846)) +* **eslint-plugin:** add no-non-null-asserted-optional-chain ([#1469](https://github.com/typescript-eslint/typescript-eslint/issues/1469)) ([498aa24](https://github.com/typescript-eslint/typescript-eslint/commit/498aa24)) +* **experimental-utils:** expose getParserServices from utils ([#1448](https://github.com/typescript-eslint/typescript-eslint/issues/1448)) ([982c8bc](https://github.com/typescript-eslint/typescript-eslint/commit/982c8bc)) + + + + + # [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 7049cb7ba6e4..1465b812eac0 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin", - "version": "2.16.0", + "version": "2.17.0", "description": "TypeScript plugin for ESLint", "keywords": [ "eslint", @@ -40,7 +40,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "2.16.0", + "@typescript-eslint/experimental-utils": "2.17.0", "eslint-utils": "^1.4.3", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", diff --git a/packages/experimental-utils/CHANGELOG.md b/packages/experimental-utils/CHANGELOG.md index bb3baf882752..161617a9f47c 100644 --- a/packages/experimental-utils/CHANGELOG.md +++ b/packages/experimental-utils/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.16.0...v2.17.0) (2020-01-20) + + +### Features + +* **experimental-utils:** expose getParserServices from utils ([#1448](https://github.com/typescript-eslint/typescript-eslint/issues/1448)) ([982c8bc](https://github.com/typescript-eslint/typescript-eslint/commit/982c8bc)) + + + + + # [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) diff --git a/packages/experimental-utils/package.json b/packages/experimental-utils/package.json index eaf57fde6a17..7513607b1114 100644 --- a/packages/experimental-utils/package.json +++ b/packages/experimental-utils/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/experimental-utils", - "version": "2.16.0", + "version": "2.17.0", "description": "(Experimental) Utilities for working with TypeScript + ESLint together", "keywords": [ "eslint", @@ -37,7 +37,7 @@ }, "dependencies": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.16.0", + "@typescript-eslint/typescript-estree": "2.17.0", "eslint-scope": "^5.0.0" }, "peerDependencies": { diff --git a/packages/parser/CHANGELOG.md b/packages/parser/CHANGELOG.md index 9655080e4d30..f103b12122b2 100644 --- a/packages/parser/CHANGELOG.md +++ b/packages/parser/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.16.0...v2.17.0) (2020-01-20) + +**Note:** Version bump only for package @typescript-eslint/parser + + + + + # [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) diff --git a/packages/parser/package.json b/packages/parser/package.json index c41abe25dd5e..32dbb0f156cc 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/parser", - "version": "2.16.0", + "version": "2.17.0", "description": "An ESLint custom parser which leverages TypeScript ESTree", "main": "dist/parser.js", "types": "dist/parser.d.ts", @@ -43,13 +43,13 @@ }, "dependencies": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.16.0", - "@typescript-eslint/typescript-estree": "2.16.0", + "@typescript-eslint/experimental-utils": "2.17.0", + "@typescript-eslint/typescript-estree": "2.17.0", "eslint-visitor-keys": "^1.1.0" }, "devDependencies": { "@types/glob": "^7.1.1", - "@typescript-eslint/shared-fixtures": "2.16.0", + "@typescript-eslint/shared-fixtures": "2.17.0", "glob": "*" }, "peerDependenciesMeta": { diff --git a/packages/shared-fixtures/CHANGELOG.md b/packages/shared-fixtures/CHANGELOG.md index df6ff2f9c8b7..deef63fffb1e 100644 --- a/packages/shared-fixtures/CHANGELOG.md +++ b/packages/shared-fixtures/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.16.0...v2.17.0) (2020-01-20) + +**Note:** Version bump only for package @typescript-eslint/shared-fixtures + + + + + # [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) **Note:** Version bump only for package @typescript-eslint/shared-fixtures diff --git a/packages/shared-fixtures/package.json b/packages/shared-fixtures/package.json index 8ce0c765b691..3af6d1d74fd7 100644 --- a/packages/shared-fixtures/package.json +++ b/packages/shared-fixtures/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/shared-fixtures", - "version": "2.16.0", + "version": "2.17.0", "private": true, "scripts": { "build": "tsc -b tsconfig.build.json", diff --git a/packages/typescript-estree/CHANGELOG.md b/packages/typescript-estree/CHANGELOG.md index df1e3a83cc90..87b3504aa95f 100644 --- a/packages/typescript-estree/CHANGELOG.md +++ b/packages/typescript-estree/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.16.0...v2.17.0) (2020-01-20) + + +### Bug Fixes + +* **typescript-estree:** correct type of `ArrayPattern.elements` ([#1451](https://github.com/typescript-eslint/typescript-eslint/issues/1451)) ([62e4ca0](https://github.com/typescript-eslint/typescript-eslint/commit/62e4ca0)) + + + + + # [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index ed9b62857559..6068d86569a9 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/typescript-estree", - "version": "2.16.0", + "version": "2.17.0", "description": "A parser that converts TypeScript source code into an ESTree compatible form", "main": "dist/parser.js", "types": "dist/parser.d.ts", @@ -58,7 +58,7 @@ "@types/lodash": "^4.14.149", "@types/semver": "^6.2.0", "@types/tmp": "^0.1.0", - "@typescript-eslint/shared-fixtures": "2.16.0", + "@typescript-eslint/shared-fixtures": "2.17.0", "tmp": "^0.1.0", "typescript": "*" },