From 835378e505f462d965ce35cc4c81f8eee1704a30 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 25 Nov 2019 11:52:50 -0800 Subject: [PATCH 01/16] chore: turn on prefer-nullish-coalescing locally (#1259) --- .eslintrc.js | 1 + .../eslint-plugin-tslint/src/rules/config.ts | 4 ++-- packages/eslint-plugin/src/rules/array-type.ts | 2 +- packages/eslint-plugin/src/rules/camelcase.ts | 9 +++++---- .../src/rules/consistent-type-definitions.ts | 4 ++-- .../src/rules/explicit-member-accessibility.ts | 16 ++++++++-------- .../src/rules/func-call-spacing.ts | 2 +- .../src/rules/member-delimiter-style.ts | 4 ++-- .../eslint-plugin/src/rules/member-ordering.ts | 8 ++++---- .../src/rules/no-unnecessary-qualifier.ts | 2 +- .../src/rules/no-unused-vars-experimental.ts | 2 +- .../src/rules/space-before-function-paren.ts | 6 +++--- .../src/rules/type-annotation-spacing.ts | 2 +- .../src/rules/unified-signatures.ts | 2 +- packages/eslint-plugin/src/util/types.ts | 2 +- .../tests/rules/no-explicit-any.test.ts | 2 +- packages/parser/src/analyze-scope.ts | 2 +- packages/typescript-estree/src/convert.ts | 18 +++++++++--------- .../src/create-program/createWatchProgram.ts | 2 +- .../typescript-estree/src/simple-traverse.ts | 2 +- .../tests/ast-alignment/fixtures-to-test.ts | 6 +++--- 21 files changed, 50 insertions(+), 48 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 20b6b1f86903..8f04af21654e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -28,6 +28,7 @@ module.exports = { '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/prefer-nullish-coalescing': 'error', '@typescript-eslint/prefer-optional-chain': 'error', '@typescript-eslint/unbound-method': 'off', diff --git a/packages/eslint-plugin-tslint/src/rules/config.ts b/packages/eslint-plugin-tslint/src/rules/config.ts index 23ff428111d2..315cf0fb4a2e 100644 --- a/packages/eslint-plugin-tslint/src/rules/config.ts +++ b/packages/eslint-plugin-tslint/src/rules/config.ts @@ -47,8 +47,8 @@ const tslintConfig = memoize( return Configuration.loadConfigurationFromPath(lintFile); } return Configuration.parseConfigFile({ - rules: tslintRules || {}, - rulesDirectory: tslintRulesDirectory || [], + rules: tslintRules ?? {}, + rulesDirectory: tslintRulesDirectory ?? [], }); }, (lintFile: string | undefined, tslintRules = {}, tslintRulesDirectory = []) => diff --git a/packages/eslint-plugin/src/rules/array-type.ts b/packages/eslint-plugin/src/rules/array-type.ts index 0ed4266a240d..de3cca753436 100644 --- a/packages/eslint-plugin/src/rules/array-type.ts +++ b/packages/eslint-plugin/src/rules/array-type.ts @@ -127,7 +127,7 @@ export default util.createRule({ const sourceCode = context.getSourceCode(); const defaultOption = options.default; - const readonlyOption = options.readonly || defaultOption; + const readonlyOption = options.readonly ?? defaultOption; const isArraySimpleOption = defaultOption === 'array-simple' && readonlyOption === 'array-simple'; diff --git a/packages/eslint-plugin/src/rules/camelcase.ts b/packages/eslint-plugin/src/rules/camelcase.ts index a2be8a1c6958..17b88d7e93d9 100644 --- a/packages/eslint-plugin/src/rules/camelcase.ts +++ b/packages/eslint-plugin/src/rules/camelcase.ts @@ -52,10 +52,11 @@ export default util.createRule({ const genericType = options.genericType; const properties = options.properties; - const allow = (options.allow || []).map(entry => ({ - name: entry, - regex: new RegExp(entry), - })); + const allow = + options.allow?.map(entry => ({ + name: entry, + regex: new RegExp(entry), + })) ?? []; /** * Checks if a string contains an underscore and isn't all upper-case diff --git a/packages/eslint-plugin/src/rules/consistent-type-definitions.ts b/packages/eslint-plugin/src/rules/consistent-type-definitions.ts index cf9e851c81cf..f90d1c566130 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-definitions.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-definitions.ts @@ -36,7 +36,7 @@ export default util.createRule({ node: node.id, messageId: 'interfaceOverType', fix(fixer) { - const typeNode = node.typeParameters || node.id; + const typeNode = node.typeParameters ?? node.id; const fixes: TSESLint.RuleFix[] = []; const firstToken = sourceCode.getFirstToken(node); @@ -70,7 +70,7 @@ export default util.createRule({ node: node.id, messageId: 'typeOverInterface', fix(fixer) { - const typeNode = node.typeParameters || node.id; + const typeNode = node.typeParameters ?? node.id; const fixes: TSESLint.RuleFix[] = []; const firstToken = sourceCode.getFirstToken(node); diff --git a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts index 864c630da178..3d44d0b61acd 100644 --- a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts +++ b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts @@ -75,14 +75,14 @@ export default util.createRule({ defaultOptions: [{ accessibility: 'explicit' }], create(context, [option]) { const sourceCode = context.getSourceCode(); - const baseCheck: AccessibilityLevel = option.accessibility || 'explicit'; - const overrides = option.overrides || {}; - const ctorCheck = overrides.constructors || baseCheck; - const accessorCheck = overrides.accessors || baseCheck; - const methodCheck = overrides.methods || baseCheck; - const propCheck = overrides.properties || baseCheck; - const paramPropCheck = overrides.parameterProperties || baseCheck; - const ignoredMethodNames = new Set(option.ignoredMethodNames || []); + const baseCheck: AccessibilityLevel = option.accessibility ?? 'explicit'; + const overrides = option.overrides ?? {}; + const ctorCheck = overrides.constructors ?? baseCheck; + const accessorCheck = overrides.accessors ?? baseCheck; + const methodCheck = overrides.methods ?? baseCheck; + const propCheck = overrides.properties ?? baseCheck; + const paramPropCheck = overrides.parameterProperties ?? baseCheck; + const ignoredMethodNames = new Set(option.ignoredMethodNames ?? []); /** * Generates the report for rule violations */ diff --git a/packages/eslint-plugin/src/rules/func-call-spacing.ts b/packages/eslint-plugin/src/rules/func-call-spacing.ts index 3a19936b7123..d76204c3fef5 100644 --- a/packages/eslint-plugin/src/rules/func-call-spacing.ts +++ b/packages/eslint-plugin/src/rules/func-call-spacing.ts @@ -82,7 +82,7 @@ export default util.createRule({ const closingParenToken = sourceCode.getLastToken(node)!; const lastCalleeTokenWithoutPossibleParens = sourceCode.getLastToken( - node.typeParameters || node.callee, + node.typeParameters ?? node.callee, )!; const openingParenToken = sourceCode.getFirstTokenBetween( lastCalleeTokenWithoutPossibleParens, diff --git a/packages/eslint-plugin/src/rules/member-delimiter-style.ts b/packages/eslint-plugin/src/rules/member-delimiter-style.ts index d1efe5e11330..7440159286f8 100644 --- a/packages/eslint-plugin/src/rules/member-delimiter-style.ts +++ b/packages/eslint-plugin/src/rules/member-delimiter-style.ts @@ -104,7 +104,7 @@ export default util.createRule({ // use the base options as the defaults for the cases const baseOptions = options; - const overrides = baseOptions.overrides || {}; + const overrides = baseOptions.overrides ?? {}; const interfaceOptions: BaseOptions = util.deepMerge( baseOptions, overrides.interface, @@ -227,7 +227,7 @@ export default util.createRule({ const opts = isSingleLine ? typeOpts.singleline : typeOpts.multiline; members.forEach((member, index) => { - checkLastToken(member, opts || {}, index === members.length - 1); + checkLastToken(member, opts ?? {}, index === members.length - 1); }); } diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index b17076856949..c8911292836b 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -388,28 +388,28 @@ export default util.createRule({ ClassDeclaration(node): void { validateMembersOrder( node.body.body, - options.classes || options.default!, + options.classes ?? options.default!, true, ); }, ClassExpression(node): void { validateMembersOrder( node.body.body, - options.classExpressions || options.default!, + options.classExpressions ?? options.default!, true, ); }, TSInterfaceDeclaration(node): void { validateMembersOrder( node.body.body, - options.interfaces || options.default!, + options.interfaces ?? options.default!, false, ); }, TSTypeLiteral(node): void { validateMembersOrder( node.members, - options.typeLiterals || options.default!, + options.typeLiterals ?? options.default!, false, ); }, diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts b/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts index 1017be32222f..45508e4692e2 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts @@ -40,7 +40,7 @@ export default util.createRule({ } function symbolIsNamespaceInScope(symbol: ts.Symbol): boolean { - const symbolDeclarations = symbol.getDeclarations() || []; + const symbolDeclarations = symbol.getDeclarations() ?? []; if ( symbolDeclarations.some(decl => diff --git a/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts b/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts index 1286982895ef..1f75e1f026f0 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts @@ -78,7 +78,7 @@ export default util.createRule({ ? new RegExp(userOptions.ignoredNamesRegex) : null, ignoreArgsIfArgsAfterAreUsed: - userOptions.ignoreArgsIfArgsAfterAreUsed || false, + userOptions.ignoreArgsIfArgsAfterAreUsed ?? false, }; function handleIdentifier(identifier: ts.Identifier): void { 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 a23865f4a17b..20ea4382002a 100644 --- a/packages/eslint-plugin/src/rules/space-before-function-paren.ts +++ b/packages/eslint-plugin/src/rules/space-before-function-paren.ts @@ -108,14 +108,14 @@ export default util.createRule({ node.async && isOpeningParenToken(sourceCode.getFirstToken(node, { skip: 1 })!) ) { - return overrideConfig.asyncArrow || baseConfig; + return overrideConfig.asyncArrow ?? baseConfig; } } else if (isNamedFunction(node)) { - return overrideConfig.named || baseConfig; + return overrideConfig.named ?? baseConfig; // `generator-star-spacing` should warn anonymous generators. E.g. `function* () {}` } else if (!node.generator) { - return overrideConfig.anonymous || baseConfig; + return overrideConfig.anonymous ?? baseConfig; } return 'ignore'; diff --git a/packages/eslint-plugin/src/rules/type-annotation-spacing.ts b/packages/eslint-plugin/src/rules/type-annotation-spacing.ts index d0f1c5b45a44..7c9162a0d05f 100644 --- a/packages/eslint-plugin/src/rules/type-annotation-spacing.ts +++ b/packages/eslint-plugin/src/rules/type-annotation-spacing.ts @@ -76,7 +76,7 @@ export default util.createRule({ const punctuators = [':', '=>']; const sourceCode = context.getSourceCode(); - const overrides = options!.overrides || { colon: {}, arrow: {} }; + const overrides = options?.overrides ?? { colon: {}, arrow: {} }; const colonOptions = Object.assign( {}, diff --git a/packages/eslint-plugin/src/rules/unified-signatures.ts b/packages/eslint-plugin/src/rules/unified-signatures.ts index c704d15b7ef2..e90490427c6c 100644 --- a/packages/eslint-plugin/src/rules/unified-signatures.ts +++ b/packages/eslint-plugin/src/rules/unified-signatures.ts @@ -500,7 +500,7 @@ export default util.createRule({ key?: string, containingNode?: ContainingNode, ): void { - key = key || getOverloadKey(signature); + key = key ?? getOverloadKey(signature); if ( currentScope && (containingNode || signature).parent === currentScope.parent diff --git a/packages/eslint-plugin/src/util/types.ts b/packages/eslint-plugin/src/util/types.ts index 2d0abcf963eb..6a3644acbb54 100644 --- a/packages/eslint-plugin/src/util/types.ts +++ b/packages/eslint-plugin/src/util/types.ts @@ -111,7 +111,7 @@ export function getConstrainedTypeAtLocation( const nodeType = checker.getTypeAtLocation(node); const constrained = checker.getBaseConstraintOfType(nodeType); - return constrained || nodeType; + return constrained ?? nodeType; } /** diff --git a/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts b/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts index 0bc8e5f3438f..d065c4109877 100644 --- a/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts +++ b/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts @@ -1009,7 +1009,7 @@ const test = >() => {}; suggestions: e.suggestions ?? suggestions(testCase.code), })), }); - const options = testCase.options || []; + const options = testCase.options ?? []; const code = `// fixToUnknown: true\n${testCase.code}`; acc.push({ code, diff --git a/packages/parser/src/analyze-scope.ts b/packages/parser/src/analyze-scope.ts index db9088604340..25cc0dfa27ee 100644 --- a/packages/parser/src/analyze-scope.ts +++ b/packages/parser/src/analyze-scope.ts @@ -879,7 +879,7 @@ export function analyzeScope( parserOptions.ecmaFeatures.globalReturn) === true, impliedStrict: false, sourceType: parserOptions.sourceType, - ecmaVersion: parserOptions.ecmaVersion || 2018, + ecmaVersion: parserOptions.ecmaVersion ?? 2018, childVisitorKeys, fallback, }; diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index c5dd74449551..25937fc89025 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -111,7 +111,7 @@ export class Converter { this.allowPattern = allowPattern; } - const result = this.convertNode(node as TSNode, parent || node.parent); + const result = this.convertNode(node as TSNode, parent ?? node.parent); this.registerTSNodeInNodeMap(node, result); @@ -1193,12 +1193,12 @@ export class Converter { if (node.dotDotDotToken) { result = this.createNode(node, { type: AST_NODE_TYPES.RestElement, - argument: this.convertChild(node.propertyName || node.name), + argument: this.convertChild(node.propertyName ?? node.name), }); } else { result = this.createNode(node, { type: AST_NODE_TYPES.Property, - key: this.convertChild(node.propertyName || node.name), + key: this.convertChild(node.propertyName ?? node.name), value: this.convertChild(node.name), computed: Boolean( node.propertyName && @@ -1387,7 +1387,7 @@ export class Converter { if (node.modifiers) { return this.createNode(node, { type: AST_NODE_TYPES.TSParameterProperty, - accessibility: getTSNodeAccessibility(node) || undefined, + accessibility: getTSNodeAccessibility(node) ?? undefined, readonly: hasModifier(SyntaxKind.ReadonlyKeyword, node) || undefined, static: hasModifier(SyntaxKind.StaticKeyword, node) || undefined, @@ -1402,7 +1402,7 @@ export class Converter { case SyntaxKind.ClassDeclaration: case SyntaxKind.ClassExpression: { - const heritageClauses = node.heritageClauses || []; + const heritageClauses = node.heritageClauses ?? []; const classNodeType = node.kind === SyntaxKind.ClassDeclaration ? AST_NODE_TYPES.ClassDeclaration @@ -1533,7 +1533,7 @@ export class Converter { return this.createNode(node, { type: AST_NODE_TYPES.ImportSpecifier, local: this.convertChild(node.name), - imported: this.convertChild(node.propertyName || node.name), + imported: this.convertChild(node.propertyName ?? node.name), }); case SyntaxKind.ImportClause: @@ -1563,7 +1563,7 @@ export class Converter { case SyntaxKind.ExportSpecifier: return this.createNode(node, { type: AST_NODE_TYPES.ExportSpecifier, - local: this.convertChild(node.propertyName || node.name), + local: this.convertChild(node.propertyName ?? node.name), exported: this.convertChild(node.name), }); @@ -1584,7 +1584,7 @@ export class Converter { case SyntaxKind.PrefixUnaryExpression: case SyntaxKind.PostfixUnaryExpression: { - const operator = (getTextForTokenKind(node.operator) || '') as any; + const operator = (getTextForTokenKind(node.operator) ?? '') as any; /** * ESTree uses UpdateExpression for ++/-- */ @@ -2375,7 +2375,7 @@ export class Converter { } case SyntaxKind.InterfaceDeclaration: { - const interfaceHeritageClauses = node.heritageClauses || []; + const interfaceHeritageClauses = node.heritageClauses ?? []; const result = this.createNode(node, { type: AST_NODE_TYPES.TSInterfaceDeclaration, body: this.createNode(node, { diff --git a/packages/typescript-estree/src/create-program/createWatchProgram.ts b/packages/typescript-estree/src/create-program/createWatchProgram.ts index 03624dfd2266..73dc8ec426e6 100644 --- a/packages/typescript-estree/src/create-program/createWatchProgram.ts +++ b/packages/typescript-estree/src/create-program/createWatchProgram.ts @@ -174,7 +174,7 @@ function getProgramsForProjects( log('Found existing program for file. %s', filePath); updatedProgram = - updatedProgram || existingWatch.getProgram().getProgram(); + updatedProgram ?? existingWatch.getProgram().getProgram(); // sets parent pointers in source files updatedProgram.getTypeChecker(); diff --git a/packages/typescript-estree/src/simple-traverse.ts b/packages/typescript-estree/src/simple-traverse.ts index e21d24d0de8b..ab4a07937d09 100644 --- a/packages/typescript-estree/src/simple-traverse.ts +++ b/packages/typescript-estree/src/simple-traverse.ts @@ -11,7 +11,7 @@ function getVisitorKeysForNode( node: TSESTree.Node, ): readonly string[] { const keys = allVisitorKeys[node.type]; - return keys || []; + return keys ?? []; } interface SimpleTraverseOptions { diff --git a/packages/typescript-estree/tests/ast-alignment/fixtures-to-test.ts b/packages/typescript-estree/tests/ast-alignment/fixtures-to-test.ts index 557ac0c11ce5..17aedafdc801 100644 --- a/packages/typescript-estree/tests/ast-alignment/fixtures-to-test.ts +++ b/packages/typescript-estree/tests/ast-alignment/fixtures-to-test.ts @@ -57,9 +57,9 @@ class FixturesTester { } } - const ignore = config.ignore || []; - const fileType = config.fileType || 'js'; - const ignoreSourceType = config.ignoreSourceType || []; + const ignore = config.ignore ?? []; + const fileType = config.fileType ?? 'js'; + const ignoreSourceType = config.ignoreSourceType ?? []; const jsx = isJSXFileType(fileType); /** From 6daff1034ecbd1f4e1662e315da7d641a7f06b11 Mon Sep 17 00:00:00 2001 From: Alexander T Date: Tue, 26 Nov 2019 13:00:04 +0200 Subject: [PATCH 02/16] fix(eslint-plugin): [no-untyped-pub-sig] ignore set return (#1264) --- .../src/rules/no-untyped-public-signature.ts | 1 + .../rules/no-untyped-public-signature.test.ts | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+) 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 39a635b850e7..b72e43e4cd04 100644 --- a/packages/eslint-plugin/src/rules/no-untyped-public-signature.ts +++ b/packages/eslint-plugin/src/rules/no-untyped-public-signature.ts @@ -108,6 +108,7 @@ export default util.createRule({ if ( node.kind !== 'constructor' && + node.kind !== 'set' && !isReturnTyped(node.value.returnType) ) { context.report({ diff --git a/packages/eslint-plugin/tests/rules/no-untyped-public-signature.test.ts b/packages/eslint-plugin/tests/rules/no-untyped-public-signature.test.ts index 164f11b4bd76..28ee928cb41c 100644 --- a/packages/eslint-plugin/tests/rules/no-untyped-public-signature.test.ts +++ b/packages/eslint-plugin/tests/rules/no-untyped-public-signature.test.ts @@ -99,6 +99,34 @@ class Foo { ` class Foo { abstract constructor(c: string) {} +} + `, + + // https://github.com/typescript-eslint/typescript-eslint/issues/1263 + ` +class Foo { + private _x: string; + + public get x(): string { + return this._x; + } + + public set x(x: string) { + this._x = x; + } +} + `, + ` +class Foo { + private _x: string; + + get x(): string { + return this._x; + } + + set x(x: string) { + this._x = x; + } } `, ], @@ -240,6 +268,40 @@ class Foo { code: ` class Foo { abstract constructor(c) {} +} + `, + errors: [{ messageId: 'untypedParameter' }], + }, + + // https://github.com/typescript-eslint/typescript-eslint/issues/1263 + { + code: ` +class Foo { + private _x: string; + + public get x(): string { + return this._x; + } + + public set x(x) { + this._x = x; + } +} + `, + errors: [{ messageId: 'untypedParameter' }], + }, + { + code: ` +class Foo { + private _x: string; + + get x(): string { + return this._x; + } + + set x(x) { + this._x = x; + } } `, errors: [{ messageId: 'untypedParameter' }], From 57ddba3008675715712b28fa03fa7f06bef2fb47 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Tue, 26 Nov 2019 09:28:55 -0800 Subject: [PATCH 03/16] fix(eslint-plugin): [prefer-optional-chain] handle more cases (#1261) --- .../src/rules/prefer-optional-chain.ts | 255 +++++++++++++++-- packages/eslint-plugin/src/util/index.ts | 1 + packages/eslint-plugin/src/util/misc.ts | 38 ++- packages/eslint-plugin/src/util/nullThrows.ts | 28 ++ .../tests/rules/prefer-optional-chain.test.ts | 261 +++++++++++------- 5 files changed, 444 insertions(+), 139 deletions(-) create mode 100644 packages/eslint-plugin/src/util/nullThrows.ts diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index a7bcc49a678b..410d41348d61 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -2,9 +2,17 @@ import { AST_NODE_TYPES, TSESTree, } from '@typescript-eslint/experimental-utils'; +import { isOpeningParenToken } from 'eslint-utils'; import * as util from '../util'; -const WHITESPACE_REGEX = /\s/g; +type ValidChainTarget = + | TSESTree.BinaryExpression + | TSESTree.CallExpression + | TSESTree.MemberExpression + | TSESTree.OptionalCallExpression + | TSESTree.OptionalMemberExpression + | TSESTree.Identifier + | TSESTree.ThisExpression; /* The AST is always constructed such the first element is always the deepest element. @@ -70,19 +78,36 @@ export default util.createRule({ let optionallyChainedCode = previousLeftText; let expressionCount = 1; while (current.type === AST_NODE_TYPES.LogicalExpression) { - if (!isValidChainTarget(current.right)) { + if ( + !isValidChainTarget( + current.right, + // only allow identifiers for the first chain - foo && foo() + expressionCount === 1, + ) + ) { break; } const leftText = previousLeftText; const rightText = getText(current.right); - if (!rightText.startsWith(leftText)) { + // can't just use startsWith because of cases like foo && fooBar.baz; + const matchRegex = new RegExp( + `^${ + // escape regex characters + leftText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + }[^a-zA-Z0-9_]`, + ); + if ( + !matchRegex.test(rightText) && + // handle redundant cases like foo.bar && foo.bar + leftText !== rightText + ) { break; } - expressionCount += 1; // omit weird doubled up expression that make no sense like foo.bar && foo.bar if (rightText !== leftText) { + expressionCount += 1; previousLeftText = rightText; /* @@ -108,21 +133,37 @@ export default util.createRule({ rightText === 'foo.bar.baz[buzz]' leftText === 'foo.bar.baz' diff === '[buzz]' + + 5) + rightText === 'foo.bar.baz?.buzz' + leftText === 'foo.bar.baz' + diff === '?.buzz' */ const diff = rightText.replace(leftText, ''); - const needsDot = diff.startsWith('(') || diff.startsWith('['); - optionallyChainedCode += `?${needsDot ? '.' : ''}${diff}`; + if (diff.startsWith('?')) { + // item was "pre optional chained" + optionallyChainedCode += diff; + } else { + const needsDot = diff.startsWith('(') || diff.startsWith('['); + optionallyChainedCode += `?${needsDot ? '.' : ''}${diff}`; + } } - /* istanbul ignore if: this shouldn't ever happen, but types */ - if (!current.parent) { - break; - } previous = current; - current = current.parent; + current = util.nullThrows( + current.parent, + util.NullThrowsReasons.MissingParent, + ); } if (expressionCount > 1) { + if (previous.right.type === AST_NODE_TYPES.BinaryExpression) { + // case like foo && foo.bar !== someValue + optionallyChainedCode += ` ${ + previous.right.operator + } ${sourceCode.getText(previous.right.right)}`; + } + context.report({ node: previous, messageId: 'preferOptionalChain', @@ -134,37 +175,191 @@ export default util.createRule({ }, }; - function getText( - node: - | TSESTree.BinaryExpression - | TSESTree.CallExpression - | TSESTree.Identifier - | TSESTree.MemberExpression, + function getText(node: ValidChainTarget): string { + if (node.type === AST_NODE_TYPES.BinaryExpression) { + return getText( + // isValidChainTarget ensures this is type safe + node.left as ValidChainTarget, + ); + } + + if ( + node.type === AST_NODE_TYPES.CallExpression || + node.type === AST_NODE_TYPES.OptionalCallExpression + ) { + const calleeText = getText( + // isValidChainTarget ensures this is type safe + node.callee as ValidChainTarget, + ); + + // ensure that the call arguments are left untouched, or else we can break cases that _need_ whitespace: + // - JSX: + // - Unary Operators: typeof foo, await bar, delete baz + const closingParenToken = util.nullThrows( + sourceCode.getLastToken(node), + util.NullThrowsReasons.MissingToken('closing parenthesis', node.type), + ); + const openingParenToken = util.nullThrows( + sourceCode.getFirstTokenBetween( + node.callee, + closingParenToken, + isOpeningParenToken, + ), + util.NullThrowsReasons.MissingToken('opening parenthesis', node.type), + ); + + const argumentsText = sourceCode.text.substring( + openingParenToken.range[0], + closingParenToken.range[1], + ); + + return `${calleeText}${argumentsText}`; + } + + if (node.type === AST_NODE_TYPES.Identifier) { + return node.name; + } + + if (node.type === AST_NODE_TYPES.ThisExpression) { + return 'this'; + } + + return getMemberExpressionText(node); + } + + /** + * Gets a normalised representation of the given MemberExpression + */ + function getMemberExpressionText( + node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression, ): string { - const text = sourceCode.getText( - node.type === AST_NODE_TYPES.BinaryExpression ? node.left : node, - ); + let objectText: string; + + // cases should match the list in ALLOWED_MEMBER_OBJECT_TYPES + switch (node.object.type) { + case AST_NODE_TYPES.CallExpression: + case AST_NODE_TYPES.OptionalCallExpression: + case AST_NODE_TYPES.Identifier: + objectText = getText(node.object); + break; + + case AST_NODE_TYPES.MemberExpression: + case AST_NODE_TYPES.OptionalMemberExpression: + objectText = getMemberExpressionText(node.object); + break; + + case AST_NODE_TYPES.ThisExpression: + objectText = getText(node.object); + break; + + /* istanbul ignore next */ + default: + throw new Error(`Unexpected member object type: ${node.object.type}`); + } + + let propertyText: string; + if (node.computed) { + // cases should match the list in ALLOWED_COMPUTED_PROP_TYPES + switch (node.property.type) { + case AST_NODE_TYPES.Identifier: + propertyText = getText(node.property); + break; - // Removes spaces from the source code for the given node - return text.replace(WHITESPACE_REGEX, ''); + case AST_NODE_TYPES.Literal: + case AST_NODE_TYPES.BigIntLiteral: + case AST_NODE_TYPES.TemplateLiteral: + propertyText = sourceCode.getText(node.property); + break; + + case AST_NODE_TYPES.MemberExpression: + case AST_NODE_TYPES.OptionalMemberExpression: + propertyText = getMemberExpressionText(node.property); + break; + + /* istanbul ignore next */ + default: + throw new Error( + `Unexpected member property type: ${node.object.type}`, + ); + } + + return `${objectText}${node.optional ? '?.' : ''}[${propertyText}]`; + } else { + // cases should match the list in ALLOWED_NON_COMPUTED_PROP_TYPES + switch (node.property.type) { + case AST_NODE_TYPES.Identifier: + propertyText = getText(node.property); + break; + + /* istanbul ignore next */ + default: + throw new Error( + `Unexpected member property type: ${node.object.type}`, + ); + } + + return `${objectText}${node.optional ? '?.' : '.'}${propertyText}`; + } } }, }); +const ALLOWED_MEMBER_OBJECT_TYPES: ReadonlySet = new Set([ + AST_NODE_TYPES.CallExpression, + AST_NODE_TYPES.Identifier, + AST_NODE_TYPES.MemberExpression, + AST_NODE_TYPES.OptionalCallExpression, + AST_NODE_TYPES.OptionalMemberExpression, + AST_NODE_TYPES.ThisExpression, +]); +const ALLOWED_COMPUTED_PROP_TYPES: ReadonlySet = new Set([ + AST_NODE_TYPES.BigIntLiteral, + AST_NODE_TYPES.Identifier, + AST_NODE_TYPES.Literal, + AST_NODE_TYPES.MemberExpression, + AST_NODE_TYPES.OptionalMemberExpression, + AST_NODE_TYPES.TemplateLiteral, +]); +const ALLOWED_NON_COMPUTED_PROP_TYPES: ReadonlySet = new Set([ + AST_NODE_TYPES.Identifier, +]); + function isValidChainTarget( node: TSESTree.Node, - allowIdentifier = false, -): node is - | TSESTree.BinaryExpression - | TSESTree.CallExpression - | TSESTree.MemberExpression { + allowIdentifier: boolean, +): node is ValidChainTarget { if ( node.type === AST_NODE_TYPES.MemberExpression || - node.type === AST_NODE_TYPES.CallExpression + node.type === AST_NODE_TYPES.OptionalMemberExpression ) { - return true; + const isObjectValid = + ALLOWED_MEMBER_OBJECT_TYPES.has(node.object.type) && + // make sure to validate the expression is of our expected structure + isValidChainTarget(node.object, true); + const isPropertyValid = node.computed + ? ALLOWED_COMPUTED_PROP_TYPES.has(node.property.type) && + // make sure to validate the member expression is of our expected structure + (node.property.type === AST_NODE_TYPES.MemberExpression || + node.property.type === AST_NODE_TYPES.OptionalMemberExpression + ? isValidChainTarget(node.property, allowIdentifier) + : true) + : ALLOWED_NON_COMPUTED_PROP_TYPES.has(node.property.type); + + return isObjectValid && isPropertyValid; } - if (allowIdentifier && node.type === AST_NODE_TYPES.Identifier) { + + if ( + node.type === AST_NODE_TYPES.CallExpression || + node.type === AST_NODE_TYPES.OptionalCallExpression + ) { + return isValidChainTarget(node.callee, allowIdentifier); + } + + if ( + allowIdentifier && + (node.type === AST_NODE_TYPES.Identifier || + node.type === AST_NODE_TYPES.ThisExpression) + ) { return true; } diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index b1aae71b3571..cb0430c114d7 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -4,6 +4,7 @@ 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 diff --git a/packages/eslint-plugin/src/util/misc.ts b/packages/eslint-plugin/src/util/misc.ts index 58c41249d473..f7c8c0194ba5 100644 --- a/packages/eslint-plugin/src/util/misc.ts +++ b/packages/eslint-plugin/src/util/misc.ts @@ -11,14 +11,14 @@ import { /** * Check if the context file name is *.d.ts or *.d.tsx */ -export function isDefinitionFile(fileName: string): boolean { +function isDefinitionFile(fileName: string): boolean { return /\.d\.tsx?$/i.test(fileName || ''); } /** * Upper cases the first character or the string */ -export function upperCaseFirst(str: string): string { +function upperCaseFirst(str: string): string { return str[0].toUpperCase() + str.slice(1); } @@ -31,7 +31,7 @@ type InferOptionsTypeFromRuleNever = T extends TSESLint.RuleModule< /** * Uses type inference to fetch the TOptions type from the given RuleModule */ -export type InferOptionsTypeFromRule = T extends TSESLint.RuleModule< +type InferOptionsTypeFromRule = T extends TSESLint.RuleModule< string, infer TOptions > @@ -41,7 +41,7 @@ export type InferOptionsTypeFromRule = T extends TSESLint.RuleModule< /** * Uses type inference to fetch the TMessageIds type from the given RuleModule */ -export type InferMessageIdsTypeFromRule = T extends TSESLint.RuleModule< +type InferMessageIdsTypeFromRule = T extends TSESLint.RuleModule< infer TMessageIds, unknown[] > @@ -51,9 +51,7 @@ export type InferMessageIdsTypeFromRule = T extends TSESLint.RuleModule< /** * Gets a string name representation of the given PropertyName node */ -export function getNameFromPropertyName( - propertyName: TSESTree.PropertyName, -): string { +function getNameFromPropertyName(propertyName: TSESTree.PropertyName): string { if (propertyName.type === AST_NODE_TYPES.Identifier) { return propertyName.name; } @@ -61,9 +59,9 @@ export function getNameFromPropertyName( } /** Return true if both parameters are equal. */ -export type Equal = (a: T, b: T) => boolean; +type Equal = (a: T, b: T) => boolean; -export function arraysAreEqual( +function arraysAreEqual( a: T[] | undefined, b: T[] | undefined, eq: (a: T, b: T) => boolean, @@ -78,7 +76,7 @@ export function arraysAreEqual( } /** Returns the first non-`undefined` result. */ -export function findFirstResult( +function findFirstResult( inputs: T[], getResult: (t: T) => U | undefined, ): U | undefined { @@ -95,7 +93,7 @@ export function findFirstResult( * Gets a string name representation of the name of the given MethodDefinition * or ClassProperty node, with handling for computed property names. */ -export function getNameFromClassMember( +function getNameFromClassMember( methodDefinition: | TSESTree.MethodDefinition | TSESTree.ClassProperty @@ -122,11 +120,25 @@ function keyCanBeReadAsPropertyName( ); } -export type ExcludeKeys< +type ExcludeKeys< TObj extends Record, TKeys extends keyof TObj > = { [k in Exclude]: TObj[k] }; -export type RequireKeys< +type RequireKeys< TObj extends Record, TKeys extends keyof TObj > = ExcludeKeys & { [k in TKeys]-?: Exclude }; + +export { + arraysAreEqual, + Equal, + ExcludeKeys, + findFirstResult, + getNameFromClassMember, + getNameFromPropertyName, + InferMessageIdsTypeFromRule, + InferOptionsTypeFromRule, + isDefinitionFile, + RequireKeys, + upperCaseFirst, +}; diff --git a/packages/eslint-plugin/src/util/nullThrows.ts b/packages/eslint-plugin/src/util/nullThrows.ts new file mode 100644 index 000000000000..df644c2befb0 --- /dev/null +++ b/packages/eslint-plugin/src/util/nullThrows.ts @@ -0,0 +1,28 @@ +/** + * A set of common reasons for calling nullThrows + */ +const NullThrowsReasons = { + MissingParent: 'Expected node to have a parent.', + MissingToken: (token: string, thing: string) => + `Expected to find a ${token} for the ${thing}.`, +} as const; + +/** + * Assert that a value must not be null or undefined. + * This is a nice explicit alternative to the non-null assertion operator. + */ +function nullThrows(value: T | null | undefined, message: string): T { + // this function is primarily used to keep types happy in a safe way + // i.e. is used when we expect that a value is never nullish + // this means that it's pretty much impossible to test the below if... + + // so ignore it in coverage metrics. + /* istanbul ignore if */ + if (value === null || value === undefined) { + throw new Error(`Non-null Assertion Failed: ${message}`); + } + + return value; +} + +export { nullThrows, NullThrowsReasons }; diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts index 2cadf43883e7..33fa13f97ce0 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts @@ -13,136 +13,93 @@ const ruleTester = new RuleTester({ const baseCases = [ // chained members { - code: ` - foo && foo.bar - `, - output: ` - foo?.bar - `, + code: 'foo && foo.bar', + output: 'foo?.bar', }, { - code: ` - foo && foo() - `, - output: ` - foo?.() - `, + code: 'foo && foo()', + output: 'foo?.()', }, { - code: ` - foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz - `, - output: ` - foo?.bar?.baz?.buzz - `, + code: 'foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz', + output: 'foo?.bar?.baz?.buzz', }, { // case with a jump (i.e. a non-nullish prop) - code: ` - foo && foo.bar && foo.bar.baz.buzz - `, - output: ` - foo?.bar?.baz.buzz - `, + code: 'foo && foo.bar && foo.bar.baz.buzz', + output: 'foo?.bar?.baz.buzz', }, { // case where for some reason there is a doubled up expression - code: ` - foo && foo.bar && foo.bar.baz && foo.bar.baz && foo.bar.baz.buzz - `, - output: ` - foo?.bar?.baz?.buzz - `, + code: 'foo && foo.bar && foo.bar.baz && foo.bar.baz && foo.bar.baz.buzz', + output: 'foo?.bar?.baz?.buzz', }, // chained members with element access { - code: ` - foo && foo[bar] && foo[bar].baz && foo[bar].baz.buzz - `, - output: ` - foo?.[bar]?.baz?.buzz - `, + code: 'foo && foo[bar] && foo[bar].baz && foo[bar].baz.buzz', + output: 'foo?.[bar]?.baz?.buzz', }, { // case with a jump (i.e. a non-nullish prop) - code: ` - foo && foo[bar].baz && foo[bar].baz.buzz - `, - output: ` - foo?.[bar].baz?.buzz - `, + code: 'foo && foo[bar].baz && foo[bar].baz.buzz', + output: 'foo?.[bar].baz?.buzz', }, // chained calls { - code: ` - foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz() - `, - output: ` - foo?.bar?.baz?.buzz() - `, + code: 'foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz()', + output: 'foo?.bar?.baz?.buzz()', }, { - code: ` - foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz && foo.bar.baz.buzz() - `, - output: ` - foo?.bar?.baz?.buzz?.() - `, + code: + 'foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz && foo.bar.baz.buzz()', + output: 'foo?.bar?.baz?.buzz?.()', }, { // case with a jump (i.e. a non-nullish prop) - code: ` - foo && foo.bar && foo.bar.baz.buzz() - `, - output: ` - foo?.bar?.baz.buzz() - `, + code: 'foo && foo.bar && foo.bar.baz.buzz()', + output: 'foo?.bar?.baz.buzz()', }, { // case with a jump (i.e. a non-nullish prop) - code: ` - foo && foo.bar && foo.bar.baz.buzz && foo.bar.baz.buzz() - `, - output: ` - foo?.bar?.baz.buzz?.() - `, + code: 'foo && foo.bar && foo.bar.baz.buzz && foo.bar.baz.buzz()', + output: 'foo?.bar?.baz.buzz?.()', }, { // case with a call expr inside the chain for some inefficient reason - code: ` - foo && foo.bar() && foo.bar().baz && foo.bar().baz.buzz && foo.bar().baz.buzz() - `, - output: ` - foo?.bar()?.baz?.buzz?.() - `, + code: + 'foo && foo.bar() && foo.bar().baz && foo.bar().baz.buzz && foo.bar().baz.buzz()', + output: 'foo?.bar()?.baz?.buzz?.()', }, // chained calls with element access { - code: ` - foo && foo.bar && foo.bar.baz && foo.bar.baz[buzz]() - `, - output: ` - foo?.bar?.baz?.[buzz]() - `, + code: 'foo && foo.bar && foo.bar.baz && foo.bar.baz[buzz]()', + output: 'foo?.bar?.baz?.[buzz]()', }, { - code: ` - foo && foo.bar && foo.bar.baz && foo.bar.baz[buzz] && foo.bar.baz[buzz]() - `, - output: ` - foo?.bar?.baz?.[buzz]?.() - `, + code: + 'foo && foo.bar && foo.bar.baz && foo.bar.baz[buzz] && foo.bar.baz[buzz]()', + output: 'foo?.bar?.baz?.[buzz]?.()', }, // two-for-one { - code: ` - foo && foo.bar && foo.bar.baz || baz && baz.bar && baz.bar.foo - `, - output: ` - foo?.bar?.baz || baz?.bar?.foo - `, + code: 'foo && foo.bar && foo.bar.baz || baz && baz.bar && baz.bar.foo', + output: 'foo?.bar?.baz || baz?.bar?.foo', errors: 2, }, + // (partially) pre-optional chained + { + code: + 'foo && foo?.bar && foo?.bar.baz && foo?.bar.baz[buzz] && foo?.bar.baz[buzz]()', + output: 'foo?.bar?.baz?.[buzz]?.()', + }, + { + code: 'foo && foo?.bar.baz && foo?.bar.baz[buzz]', + output: 'foo?.bar.baz?.[buzz]', + }, + { + code: 'foo && foo?.() && foo?.().bar', + output: 'foo?.()?.bar', + }, ].map( c => ({ @@ -167,6 +124,14 @@ ruleTester.run('prefer-optional-chain', rule, { 'foo ?? foo.bar', "file !== 'index.ts' && file.endsWith('.ts')", 'nextToken && sourceCode.isSpaceBetweenTokens(prevToken, nextToken)', + 'result && this.options.shouldPreserveNodeMaps', + 'foo && fooBar.baz', + 'foo !== null && foo !== undefined', + 'x["y"] !== undefined && x["y"] !== null', + // currently do not handle complex computed properties + 'foo && foo[bar as string] && foo[bar as string].baz', + 'foo && foo[1 + 2] && foo[1 + 2].baz', + 'foo && foo[typeof bar] && foo[typeof bar].baz', ], invalid: [ ...baseCases, @@ -193,27 +158,93 @@ ruleTester.run('prefer-optional-chain', rule, { // strict nullish equality checks x !== null && x.y !== null ...baseCases.map(c => ({ ...c, - code: c.code.replace(/&&/g, ' !== null &&'), + code: c.code.replace(/&&/g, '!== null &&'), })), ...baseCases.map(c => ({ ...c, - code: c.code.replace(/&&/g, ' != null &&'), + code: c.code.replace(/&&/g, '!= null &&'), })), ...baseCases.map(c => ({ ...c, - code: c.code.replace(/&&/g, ' !== undefined &&'), + code: c.code.replace(/&&/g, '!== undefined &&'), })), ...baseCases.map(c => ({ ...c, - code: c.code.replace(/&&/g, ' != undefined &&'), + code: c.code.replace(/&&/g, '!= undefined &&'), })), { // case with inconsistent checks + code: + 'foo && foo.bar != null && foo.bar.baz !== undefined && foo.bar.baz.buzz', + output: 'foo?.bar?.baz?.buzz', + errors: [ + { + messageId: 'preferOptionalChain', + }, + ], + }, + // ensure essential whitespace isn't removed + { + code: 'foo && foo.bar(baz => ());', + output: 'foo?.bar(baz => ());', + errors: [ + { + messageId: 'preferOptionalChain', + }, + ], + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + { + code: 'foo && foo.bar(baz => typeof baz);', + output: 'foo?.bar(baz => typeof baz);', + errors: [ + { + messageId: 'preferOptionalChain', + }, + ], + }, + { + code: 'foo && foo["some long string"] && foo["some long string"].baz', + output: 'foo?.["some long string"]?.baz', + errors: [ + { + messageId: 'preferOptionalChain', + }, + ], + }, + { + code: 'foo && foo[`some long string`] && foo[`some long string`].baz', + output: 'foo?.[`some long string`]?.baz', + errors: [ + { + messageId: 'preferOptionalChain', + }, + ], + }, + { + code: "foo && foo['some long string'] && foo['some long string'].baz", + output: "foo?.['some long string']?.baz", + errors: [ + { + messageId: 'preferOptionalChain', + }, + ], + }, + // should preserve comments in a call expression + { code: ` - foo && foo.bar != null && foo.bar.baz && foo.bar.baz.buzz !== undefined + foo && foo.bar(/* comment */a, + // comment2 + b, ); `, output: ` - foo?.bar?.baz?.buzz + foo?.bar(/* comment */a, + // comment2 + b, ); `, errors: [ { @@ -221,5 +252,43 @@ ruleTester.run('prefer-optional-chain', rule, { }, ], }, + // ensure binary expressions that are the last expression do not get removed + { + code: 'foo && foo.bar != null', + output: 'foo?.bar != null', + errors: [ + { + messageId: 'preferOptionalChain', + }, + ], + }, + { + code: 'foo && foo.bar != undefined', + output: 'foo?.bar != undefined', + errors: [ + { + messageId: 'preferOptionalChain', + }, + ], + }, + { + code: 'foo && foo.bar != null && baz', + output: 'foo?.bar != null && baz', + errors: [ + { + messageId: 'preferOptionalChain', + }, + ], + }, + // other weird cases + { + code: 'foo && foo?.()', + output: 'foo?.()', + errors: [ + { + messageId: 'preferOptionalChain', + }, + ], + }, ], }); From f83f04b78a88dadde7e1885a0d20ab0e2f458b8e Mon Sep 17 00:00:00 2001 From: Alexander T Date: Tue, 26 Nov 2019 19:50:41 +0200 Subject: [PATCH 04/16] =?UTF-8?q?feat(experimental-utils):=20add=20isSpace?= =?UTF-8?q?Between=20declaration=20to=20Sou=E2=80=A6=20(#1268)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/experimental-utils/src/ts-eslint/SourceCode.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/experimental-utils/src/ts-eslint/SourceCode.ts b/packages/experimental-utils/src/ts-eslint/SourceCode.ts index ac8331166063..6744e988d8d4 100644 --- a/packages/experimental-utils/src/ts-eslint/SourceCode.ts +++ b/packages/experimental-utils/src/ts-eslint/SourceCode.ts @@ -32,6 +32,14 @@ declare interface SourceCode { getNodeByRangeIndex(index: number): TSESTree.Node | null; + isSpaceBetween( + first: TSESTree.Token | TSESTree.Node, + second: TSESTree.Token | TSESTree.Node, + ): boolean; + + /** + * @deprecated in favor of isSpaceBetween() + */ isSpaceBetweenTokens(first: TSESTree.Token, second: TSESTree.Token): boolean; getLocFromIndex(index: number): TSESTree.LineAndColumnData; From c72c3c1ade7d9dd42e222c85075e06a79b95260b Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Tue, 26 Nov 2019 16:26:32 -0800 Subject: [PATCH 05/16] fix(eslint-plugin): [prefer-optional-chain] allow $ in identifiers --- packages/eslint-plugin/src/rules/prefer-optional-chain.ts | 2 +- .../eslint-plugin/tests/rules/prefer-optional-chain.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 410d41348d61..5f8ef5bc0585 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -95,7 +95,7 @@ export default util.createRule({ `^${ // escape regex characters leftText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - }[^a-zA-Z0-9_]`, + }[^a-zA-Z0-9_$]`, ); if ( !matchRegex.test(rightText) && diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts index 33fa13f97ce0..ccd3e992cf01 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts @@ -126,6 +126,7 @@ ruleTester.run('prefer-optional-chain', rule, { 'nextToken && sourceCode.isSpaceBetweenTokens(prevToken, nextToken)', 'result && this.options.shouldPreserveNodeMaps', 'foo && fooBar.baz', + 'match && match$1 !== undefined', 'foo !== null && foo !== undefined', 'x["y"] !== undefined && x["y"] !== null', // currently do not handle complex computed properties From 3b931acf563cbf0e32508767ef9895011eeab4b9 Mon Sep 17 00:00:00 2001 From: Alexander T Date: Wed, 27 Nov 2019 08:24:30 +0200 Subject: [PATCH 06/16] feat(eslint-plugin): [no-empty-func] private/protected construct (#1267) Co-authored-by: Brad Zacher --- .../docs/rules/no-empty-function.md | 33 +++++++ .../src/rules/no-empty-function.ts | 87 ++++++++++++------- .../tests/rules/no-empty-function.test.ts | 73 ++++++++++++++++ 3 files changed, 164 insertions(+), 29 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-empty-function.md b/packages/eslint-plugin/docs/rules/no-empty-function.md index e06961d4259b..2b37c4da396e 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-function.md +++ b/packages/eslint-plugin/docs/rules/no-empty-function.md @@ -44,4 +44,37 @@ See the [ESLint documentation](https://eslint.org/docs/rules/no-empty-function) } ``` +## Options + +This rule has an object option: + +- `allow` (`string[]`) + - `"protected-constructors"` - Protected class constructors. + - `"private-constructors"` - Private class constructors. + - [See the other options allowed](https://github.com/eslint/eslint/blob/master/docs/rules/no-empty-function.md#options) + +#### allow: protected-constructors + +Examples of **correct** code for the `{ "allow": ["protected-constructors"] }` option: + +```ts +/*eslint @typescript-eslint/no-empty-function: ["error", { "allow": ["protected-constructors"] }]*/ + +class Foo { + protected constructor() {} +} +``` + +#### allow: private-constructors + +Examples of **correct** code for the `{ "allow": ["private-constructors"] }` option: + +```ts +/*eslint @typescript-eslint/no-empty-function: ["error", { "allow": ["private-constructors"] }]*/ + +class Foo { + private constructor() {} +} +``` + Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-empty-function.md) diff --git a/packages/eslint-plugin/src/rules/no-empty-function.ts b/packages/eslint-plugin/src/rules/no-empty-function.ts index 31aa5d8bb630..b0c88342a490 100644 --- a/packages/eslint-plugin/src/rules/no-empty-function.ts +++ b/packages/eslint-plugin/src/rules/no-empty-function.ts @@ -8,6 +8,32 @@ import * as util from '../util'; type Options = util.InferOptionsTypeFromRule; type MessageIds = util.InferMessageIdsTypeFromRule; +const schema = util.deepMerge( + Array.isArray(baseRule.meta.schema) + ? baseRule.meta.schema[0] + : baseRule.meta.schema, + { + properties: { + allow: { + items: { + enum: [ + 'functions', + 'arrowFunctions', + 'generatorFunctions', + 'methods', + 'generatorMethods', + 'getters', + 'setters', + 'constructors', + 'private-constructors', + 'protected-constructors', + ], + }, + }, + }, + }, +); + export default util.createRule({ name: 'no-empty-function', meta: { @@ -17,7 +43,7 @@ export default util.createRule({ category: 'Best Practices', recommended: 'error', }, - schema: baseRule.meta.schema, + schema: [schema], messages: baseRule.meta.messages, }, defaultOptions: [ @@ -25,24 +51,13 @@ export default util.createRule({ allow: [], }, ], - create(context) { + create(context, [{ allow = [] }]) { const rules = baseRule.create(context); - /** - * Checks if the node is a constructor - * @param node the node to ve validated - * @returns true if the node is a constructor - * @private - */ - function isConstructor( - node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression, - ): boolean { - return !!( - node.parent && - node.parent.type === 'MethodDefinition' && - node.parent.kind === 'constructor' - ); - } + const isAllowedProtectedConstructors = allow.includes( + 'protected-constructors', + ); + const isAllowedPrivateConstructors = allow.includes('private-constructors'); /** * Check if the method body is empty @@ -74,30 +89,44 @@ export default util.createRule({ } /** - * Checks if the method is a concise constructor (no function body, but has parameter properties) * @param node the node to be validated - * @returns true if the method is a concise constructor + * @returns true if the constructor is allowed to be empty * @private */ - function isConciseConstructor( + function isAllowedEmptyConstructor( node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression, ): boolean { - // Check TypeScript specific nodes - return ( - isConstructor(node) && isBodyEmpty(node) && hasParameterProperties(node) - ); + const parent = node.parent; + if ( + isBodyEmpty(node) && + parent?.type === 'MethodDefinition' && + parent.kind === 'constructor' + ) { + const { accessibility } = parent; + + return ( + // allow protected constructors + (accessibility === 'protected' && isAllowedProtectedConstructors) || + // allow private constructors + (accessibility === 'private' && isAllowedPrivateConstructors) || + // allow constructors which have parameter properties + hasParameterProperties(node) + ); + } + + return false; } return { FunctionDeclaration(node): void { - if (!isConciseConstructor(node)) { - rules.FunctionDeclaration(node); - } + rules.FunctionDeclaration(node); }, FunctionExpression(node): void { - if (!isConciseConstructor(node)) { - rules.FunctionExpression(node); + if (isAllowedEmptyConstructor(node)) { + return; } + + rules.FunctionExpression(node); }, }; }, diff --git a/packages/eslint-plugin/tests/rules/no-empty-function.test.ts b/packages/eslint-plugin/tests/rules/no-empty-function.test.ts index 4b40c1708676..9b5ad3507d3a 100644 --- a/packages/eslint-plugin/tests/rules/no-empty-function.test.ts +++ b/packages/eslint-plugin/tests/rules/no-empty-function.test.ts @@ -32,6 +32,29 @@ ruleTester.run('no-empty-function', rule, { }`, options: [{ allow: ['methods'] }], }, + { + code: ` +class Foo { + private constructor() {} +} + `, + options: [{ allow: ['private-constructors'] }], + }, + { + code: ` +class Foo { + protected constructor() {} +} + `, + options: [{ allow: ['protected-constructors'] }], + }, + { + code: ` +function foo() { + const a = null; +} + `, + }, ], invalid: [ @@ -65,5 +88,55 @@ ruleTester.run('no-empty-function', rule, { }, ], }, + { + code: ` +class Foo { + private constructor() {} +} + `, + errors: [ + { + messageId: 'unexpected', + data: { + name: 'constructor', + }, + line: 3, + column: 25, + }, + ], + }, + { + code: ` +class Foo { + protected constructor() {} +} + `, + errors: [ + { + messageId: 'unexpected', + data: { + name: 'constructor', + }, + line: 3, + column: 27, + }, + ], + }, + { + code: ` +function foo() { +} + `, + errors: [ + { + messageId: 'unexpected', + data: { + name: "function 'foo'", + }, + line: 2, + column: 16, + }, + ], + }, ], }); From e9d44f51efcede276ba45462c10f93cea91ae2f0 Mon Sep 17 00:00:00 2001 From: Alexander T Date: Wed, 27 Nov 2019 20:10:55 +0200 Subject: [PATCH 07/16] fix(eslint-plugin): [no-empty-function] add missed node types (#1271) --- .eslintrc.js | 6 ++++++ packages/eslint-plugin/src/rules/no-empty-function.ts | 4 +--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 8f04af21654e..42d8a09253b1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -32,6 +32,12 @@ module.exports = { '@typescript-eslint/prefer-optional-chain': 'error', '@typescript-eslint/unbound-method': 'off', + 'no-empty-function': 'off', + '@typescript-eslint/no-empty-function': [ + 'error', + { allow: ['arrowFunctions'] }, + ], + // // eslint base // diff --git a/packages/eslint-plugin/src/rules/no-empty-function.ts b/packages/eslint-plugin/src/rules/no-empty-function.ts index b0c88342a490..17f238abec1d 100644 --- a/packages/eslint-plugin/src/rules/no-empty-function.ts +++ b/packages/eslint-plugin/src/rules/no-empty-function.ts @@ -118,9 +118,7 @@ export default util.createRule({ } return { - FunctionDeclaration(node): void { - rules.FunctionDeclaration(node); - }, + ...rules, FunctionExpression(node): void { if (isAllowedEmptyConstructor(node)) { return; From ebf5e0a2eb5119448ded59a78d7cb9e4c3b36692 Mon Sep 17 00:00:00 2001 From: Alexander T Date: Wed, 27 Nov 2019 20:11:48 +0200 Subject: [PATCH 08/16] fix(eslint-plugin): [return-await] allow Any and Unknown (#1270) --- packages/eslint-plugin/src/rules/return-await.ts | 6 +----- .../eslint-plugin/tests/rules/return-await.test.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/return-await.ts b/packages/eslint-plugin/src/rules/return-await.ts index b96c612fd7a7..6594007651cc 100644 --- a/packages/eslint-plugin/src/rules/return-await.ts +++ b/packages/eslint-plugin/src/rules/return-await.ts @@ -68,11 +68,7 @@ export default util.createRule({ } const type = checker.getTypeAtLocation(child); - - const isThenable = - tsutils.isTypeFlagSet(type, ts.TypeFlags.Any) || - tsutils.isTypeFlagSet(type, ts.TypeFlags.Unknown) || - tsutils.isThenableType(checker, expression, type); + const isThenable = tsutils.isThenableType(checker, expression, type); if (!isAwait && !isThenable) { return; diff --git a/packages/eslint-plugin/tests/rules/return-await.test.ts b/packages/eslint-plugin/tests/rules/return-await.test.ts index bddbc85d22c0..fc5f2597ebf4 100644 --- a/packages/eslint-plugin/tests/rules/return-await.test.ts +++ b/packages/eslint-plugin/tests/rules/return-await.test.ts @@ -148,6 +148,16 @@ ruleTester.run('return-await', rule, { } }`, }, + { + code: `async function test(): Promise { + const res = await Promise.resolve('{}'); + try { + return JSON.parse(res); + } catch (error) { + return res; + } + }`, + }, ], invalid: [ { From 3b393405fe496203fb2ebf226dc5c9f2077dd8b7 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Wed, 27 Nov 2019 16:29:13 -0800 Subject: [PATCH 09/16] fix(eslint-plugin): [strict-bool-expr] allow nullish coalescing (#1275) --- .../src/rules/strict-boolean-expressions.ts | 2 +- .../tests/rules/strict-boolean-expressions.test.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts index e67e4d1f687f..1b0d0de9462c 100644 --- a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts +++ b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts @@ -150,7 +150,7 @@ export default util.createRule({ ForStatement: assertTestExpressionContainsBoolean, IfStatement: assertTestExpressionContainsBoolean, WhileStatement: assertTestExpressionContainsBoolean, - LogicalExpression: assertLocalExpressionContainsBoolean, + 'LogicalExpression[operator!="??"]': assertLocalExpressionContainsBoolean, 'UnaryExpression[operator="!"]': assertUnaryExpressionContainsBoolean, }; }, diff --git a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts index c95228b67430..f1b98a1c9411 100644 --- a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts @@ -159,11 +159,11 @@ ruleTester.run('strict-boolean-expressions', rule, { { options: [{ ignoreRhs: true }], code: ` -const obj = {}; -const bool = false; -const boolOrObj = bool || obj; -const boolAndObj = bool && obj; -`, + const obj = {}; + const bool = false; + const boolOrObj = bool || obj; + const boolAndObj = bool && obj; + `, }, { options: [{ allowNullable: true }], @@ -174,6 +174,10 @@ const boolAndObj = bool && obj; const f4 = (x?: false) => x ? 1 : 0; `, }, + ` + declare const x: string | null; + y = x ?? 'foo'; + `, ], invalid: [ From dc735104d5671e5b05c3678a40ceadc152f27c58 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 1 Dec 2019 10:47:54 +1300 Subject: [PATCH 10/16] fix(typescript-estree): make FunctionDeclaration.body non-null (#1288) --- packages/typescript-estree/src/ts-estree/ts-estree.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/typescript-estree/src/ts-estree/ts-estree.ts b/packages/typescript-estree/src/ts-estree/ts-estree.ts index afda76ead68d..295af65ee618 100644 --- a/packages/typescript-estree/src/ts-estree/ts-estree.ts +++ b/packages/typescript-estree/src/ts-estree/ts-estree.ts @@ -718,6 +718,7 @@ export interface ForStatement extends BaseNode { export interface FunctionDeclaration extends FunctionDeclarationBase { type: AST_NODE_TYPES.FunctionDeclaration; + body: BlockStatement; } export interface FunctionExpression extends FunctionDeclarationBase { From f84eb96b2733c5e047c6ac6aa5fcfef21bac1d96 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sat, 30 Nov 2019 13:50:31 -0800 Subject: [PATCH 11/16] feat(eslint-plugin): [prefer-null-coal] opt for suggestion fixer (#1272) --- .../docs/rules/prefer-nullish-coalescing.md | 12 +++- .../src/rules/prefer-nullish-coalescing.ts | 64 ++++++++++++------- .../rules/prefer-nullish-coalescing.test.ts | 28 ++++++++ 3 files changed, 79 insertions(+), 25 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md index 140ade2dbe7c..f0ef0872d4eb 100644 --- a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md +++ b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md @@ -46,13 +46,15 @@ type Options = [ { ignoreConditionalTests?: boolean; ignoreMixedLogicalExpressions?: boolean; + forceSuggestionFixer?: boolean; }, ]; const defaultOptions = [ { ignoreConditionalTests: true, - ignoreMixedLogicalExpressions: true; + ignoreMixedLogicalExpressions: true, + forceSuggestionFixer: false, }, ]; ``` @@ -129,7 +131,13 @@ a ?? (b && c) ?? d; a ?? (b && c && d); ``` -**_NOTE:_** Errors for this specific case will be presented as suggestions, instead of fixes. This is because it is not always safe to automatically convert `||` to `??` within a mixed logical expression, as we cannot tell the intended precedence of the operator. Note that by design, `??` requires parentheses when used with `&&` or `||` in the same expression. +**_NOTE:_** Errors for this specific case will be presented as suggestions (see below), instead of fixes. This is because it is not always safe to automatically convert `||` to `??` within a mixed logical expression, as we cannot tell the intended precedence of the operator. Note that by design, `??` requires parentheses when used with `&&` or `||` in the same expression. + +### forceSuggestionFixer + +Setting this option to `true` will cause the rule to use ESLint's "suggested fix" mode for all fixes. _This option is provided as to aid in transitioning your codebase onto this rule_. + +Suggestion fixes cannot be automatically applied via the `--fix` CLI command, but can be _manually_ chosen to be applied one at a time via an IDE or similar. This makes it safe to run autofixers on an existing codebase without worrying about potential runtime behaviour changes from this rule's fixer. ## When Not To Use It diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index afb564025fd7..649c8b3db21c 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -11,6 +11,7 @@ export type Options = [ { ignoreConditionalTests?: boolean; ignoreMixedLogicalExpressions?: boolean; + forceSuggestionFixer?: boolean; }, ]; export type MessageIds = 'preferNullish'; @@ -41,6 +42,9 @@ export default util.createRule({ ignoreMixedLogicalExpressions: { type: 'boolean', }, + forceSuggestionFixer: { + type: 'boolean', + }, }, additionalProperties: false, }, @@ -50,9 +54,19 @@ export default util.createRule({ { ignoreConditionalTests: true, ignoreMixedLogicalExpressions: true, + forceSuggestionFixer: false, }, ], - create(context, [{ ignoreConditionalTests, ignoreMixedLogicalExpressions }]) { + create( + context, + [ + { + ignoreConditionalTests, + ignoreMixedLogicalExpressions, + forceSuggestionFixer, + }, + ], + ) { const parserServices = util.getParserServices(context); const sourceCode = context.getSourceCode(); const checker = parserServices.program.getTypeChecker(); @@ -79,30 +93,34 @@ export default util.createRule({ return; } - const barBarOperator = sourceCode.getTokenAfter( - node.left, - token => - token.type === AST_TOKEN_TYPES.Punctuator && - token.value === node.operator, - )!; // there _must_ be an operator - - const fixer = isMixedLogical - ? // suggestion instead for cases where we aren't sure if the fixer is completely safe - ({ - suggest: [ - { - messageId: 'preferNullish', - fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { - return fixer.replaceText(barBarOperator, '??'); + const barBarOperator = util.nullThrows( + sourceCode.getTokenAfter( + node.left, + token => + token.type === AST_TOKEN_TYPES.Punctuator && + token.value === node.operator, + ), + util.NullThrowsReasons.MissingToken('operator', node.type), + ); + + const fixer = + isMixedLogical || forceSuggestionFixer + ? // suggestion instead for cases where we aren't sure if the fixer is completely safe + ({ + suggest: [ + { + messageId: 'preferNullish', + fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { + return fixer.replaceText(barBarOperator, '??'); + }, }, + ], + } as const) + : { + fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { + return fixer.replaceText(barBarOperator, '??'); }, - ], - } as const) - : { - fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { - return fixer.replaceText(barBarOperator, '??'); - }, - }; + }; context.report({ node: barBarOperator, diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 1f05d6b9a3fc..7dd9212770c6 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -435,5 +435,33 @@ if (function werid() { return x ?? 'foo' }) {} }, ], })), + + // testing the suggestion fixer option + { + code: ` +declare const x: string | null; +x || 'foo'; + `.trimRight(), + output: null, + options: [{ forceSuggestionFixer: true }], + errors: [ + { + messageId: 'preferNullish', + line: 3, + column: 3, + endLine: 3, + endColumn: 5, + suggestions: [ + { + messageId: 'preferNullish', + output: ` +declare const x: string | null; +x ?? 'foo'; + `.trimRight(), + }, + ], + }, + ], + }, ], }); From ce4c803522d5606f6e25e33206502291c7b20474 Mon Sep 17 00:00:00 2001 From: Alexander T Date: Sat, 30 Nov 2019 23:56:07 +0200 Subject: [PATCH 12/16] fix(eslint-plugin): [no-unused-expressions] ignore directives (#1285) --- .../src/rules/no-unused-expressions.ts | 11 +-- .../tests/rules/no-unused-expressions.test.ts | 76 ++++++++++++++++++- .../src/ts-estree/ts-estree.ts | 1 + 3 files changed, 82 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-expressions.ts b/packages/eslint-plugin/src/rules/no-unused-expressions.ts index f7d86f50c1c7..b2eb6f8e30b7 100644 --- a/packages/eslint-plugin/src/rules/no-unused-expressions.ts +++ b/packages/eslint-plugin/src/rules/no-unused-expressions.ts @@ -12,10 +12,7 @@ export default util.createRule({ recommended: false, }, schema: baseRule.meta.schema, - messages: { - expected: - 'Expected an assignment or function call and instead saw an expression.', - }, + messages: {}, }, defaultOptions: [], create(context) { @@ -23,9 +20,13 @@ export default util.createRule({ return { ExpressionStatement(node): void { - if (node.expression.type === AST_NODE_TYPES.OptionalCallExpression) { + if ( + node.directive || + node.expression.type === AST_NODE_TYPES.OptionalCallExpression + ) { return; } + rules.ExpressionStatement(node); }, }; diff --git a/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts b/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts index 46653b30c663..feb3a584bcc3 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts @@ -1,3 +1,4 @@ +import { TSESLint } from '@typescript-eslint/experimental-utils'; import rule from '../../src/rules/no-unused-expressions'; import { RuleTester } from '../RuleTester'; @@ -10,9 +11,11 @@ const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', }); +type TestCaseError = Omit, 'messageId'>; + // the base rule doesn't have messageIds function error( - messages: { line: number; column: number }[], + messages: TestCaseError[], // eslint-disable-next-line @typescript-eslint/no-explicit-any ): any[] { return messages.map(message => ({ @@ -42,6 +45,26 @@ ruleTester.run('no-unused-expressions', rule, { ` a?.['b']?.c(); `, + ` + module Foo { + 'use strict'; + } + `, + ` + namespace Foo { + 'use strict'; + + export class Foo {} + export class Bar {} + } + `, + ` + function foo() { + 'use strict'; + + return null; + } + `, ], invalid: [ { @@ -176,5 +199,56 @@ one.two?.three.four; }, ]), }, + { + code: ` +module Foo { + const foo = true; + 'use strict'; +} + `, + errors: error([ + { + line: 4, + endLine: 4, + column: 3, + endColumn: 16, + }, + ]), + }, + { + code: ` +namespace Foo { + export class Foo {} + export class Bar {} + + 'use strict'; +} + `, + errors: error([ + { + line: 6, + endLine: 6, + column: 3, + endColumn: 16, + }, + ]), + }, + { + code: ` +function foo() { + const foo = true; + + 'use strict'; +} + `, + errors: error([ + { + line: 5, + endLine: 5, + column: 3, + endColumn: 16, + }, + ]), + }, ], }); diff --git a/packages/typescript-estree/src/ts-estree/ts-estree.ts b/packages/typescript-estree/src/ts-estree/ts-estree.ts index 295af65ee618..33784097a01e 100644 --- a/packages/typescript-estree/src/ts-estree/ts-estree.ts +++ b/packages/typescript-estree/src/ts-estree/ts-estree.ts @@ -691,6 +691,7 @@ export interface ExportSpecifier extends BaseNode { export interface ExpressionStatement extends BaseNode { type: AST_NODE_TYPES.ExpressionStatement; expression: Expression; + directive?: string; } export interface ForInStatement extends BaseNode { From e350a21fb19d976b1cac84b097039f2e59fb26f6 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sat, 30 Nov 2019 14:02:27 -0800 Subject: [PATCH 13/16] feat(eslint-plugin): [no-non-null-assert] add suggestion fixer (#1260) --- .../src/rules/no-non-null-assertion.ts | 98 +++++- packages/eslint-plugin/src/util/astUtils.ts | 15 +- .../tests/rules/no-non-null-assertion.test.ts | 280 +++++++++++++++++- .../experimental-utils/src/ts-eslint/Rule.ts | 8 +- .../src/ts-eslint/RuleTester.ts | 2 +- 5 files changed, 393 insertions(+), 10 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-non-null-assertion.ts b/packages/eslint-plugin/src/rules/no-non-null-assertion.ts index 4e888c58bb64..ef268d7a4e2f 100644 --- a/packages/eslint-plugin/src/rules/no-non-null-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-non-null-assertion.ts @@ -1,6 +1,12 @@ +import { + TSESLint, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; import * as util from '../util'; -export default util.createRule({ +type MessageIds = 'noNonNull' | 'suggestOptionalChain'; + +export default util.createRule<[], MessageIds>({ name: 'no-non-null-assertion', meta: { type: 'problem', @@ -12,16 +18,106 @@ export default util.createRule({ }, messages: { noNonNull: 'Forbidden non-null assertion.', + suggestOptionalChain: + 'Consider using the optional chain operator `?.` instead. This operator includes runtime checks, so it is safer than the compile-only non-null assertion operator.', }, schema: [], }, defaultOptions: [], create(context) { + const sourceCode = context.getSourceCode(); return { TSNonNullExpression(node): void { + const suggest: TSESLint.ReportSuggestionArray = []; + function convertTokenToOptional( + replacement: '?' | '?.', + ): TSESLint.ReportFixFunction { + return (fixer: TSESLint.RuleFixer): TSESLint.RuleFix | null => { + const operator = sourceCode.getTokenAfter( + node.expression, + util.isNonNullAssertionPunctuator, + ); + if (operator) { + return fixer.replaceText(operator, replacement); + } + + return null; + }; + } + function removeToken(): TSESLint.ReportFixFunction { + return (fixer: TSESLint.RuleFixer): TSESLint.RuleFix | null => { + const operator = sourceCode.getTokenAfter( + node.expression, + util.isNonNullAssertionPunctuator, + ); + if (operator) { + return fixer.remove(operator); + } + + return null; + }; + } + + if (node.parent) { + if ( + (node.parent.type === AST_NODE_TYPES.MemberExpression || + node.parent.type === AST_NODE_TYPES.OptionalMemberExpression) && + node.parent.object === node + ) { + if (!node.parent.optional) { + if (node.parent.computed) { + // it is x![y]?.z + suggest.push({ + messageId: 'suggestOptionalChain', + fix: convertTokenToOptional('?.'), + }); + } else { + // it is x!.y?.z + suggest.push({ + messageId: 'suggestOptionalChain', + fix: convertTokenToOptional('?'), + }); + } + } else { + if (node.parent.computed) { + // it is x!?.[y].z + suggest.push({ + messageId: 'suggestOptionalChain', + fix: removeToken(), + }); + } else { + // it is x!?.y.z + suggest.push({ + messageId: 'suggestOptionalChain', + fix: removeToken(), + }); + } + } + } else if ( + (node.parent.type === AST_NODE_TYPES.CallExpression || + node.parent.type === AST_NODE_TYPES.OptionalCallExpression) && + node.parent.callee === node + ) { + if (!node.parent.optional) { + // it is x.y?.z!() + suggest.push({ + messageId: 'suggestOptionalChain', + fix: convertTokenToOptional('?.'), + }); + } else { + // it is x.y.z!?.() + suggest.push({ + messageId: 'suggestOptionalChain', + fix: removeToken(), + }); + } + } + } + context.report({ node, messageId: 'noNonNull', + suggest, }); }, }; diff --git a/packages/eslint-plugin/src/util/astUtils.ts b/packages/eslint-plugin/src/util/astUtils.ts index fccbafeede78..30e8231f95e2 100644 --- a/packages/eslint-plugin/src/util/astUtils.ts +++ b/packages/eslint-plugin/src/util/astUtils.ts @@ -17,6 +17,17 @@ function isNotOptionalChainPunctuator( return !isOptionalChainPunctuator(token); } +function isNonNullAssertionPunctuator( + token: TSESTree.Token | TSESTree.Comment, +): boolean { + return token.type === AST_TOKEN_TYPES.Punctuator && token.value === '!'; +} +function isNotNonNullAssertionPunctuator( + token: TSESTree.Token | TSESTree.Comment, +): boolean { + return !isNonNullAssertionPunctuator(token); +} + /** * Returns true if and only if the node represents: foo?.() or foo.bar?.() */ @@ -32,8 +43,10 @@ function isOptionalOptionalChain( } export { - LINEBREAK_MATCHER, + isNonNullAssertionPunctuator, + isNotNonNullAssertionPunctuator, isNotOptionalChainPunctuator, isOptionalChainPunctuator, isOptionalOptionalChain, + LINEBREAK_MATCHER, }; diff --git a/packages/eslint-plugin/tests/rules/no-non-null-assertion.test.ts b/packages/eslint-plugin/tests/rules/no-non-null-assertion.test.ts index ac2287a85659..3bced219288a 100644 --- a/packages/eslint-plugin/tests/rules/no-non-null-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-non-null-assertion.test.ts @@ -6,15 +6,289 @@ const ruleTester = new RuleTester({ }); ruleTester.run('no-non-null-assertion', rule, { - valid: ['const x = { y: 1 }; x.y;'], + valid: [ + // + 'x', + 'x.y', + 'x.y.z', + 'x?.y.z', + 'x?.y?.z', + '!x', + ], invalid: [ { - code: 'const x = null; x!.y;', + code: 'x!', + errors: [ + { + messageId: 'noNonNull', + line: 1, + column: 1, + suggestions: undefined, + }, + ], + }, + { + code: 'x!.y', + errors: [ + { + messageId: 'noNonNull', + line: 1, + column: 1, + suggestions: [ + { + messageId: 'suggestOptionalChain', + output: 'x?.y', + }, + ], + }, + ], + }, + { + code: 'x.y!', + errors: [ + { + messageId: 'noNonNull', + line: 1, + column: 1, + suggestions: undefined, + }, + ], + }, + { + code: '!x!.y', + errors: [ + { + messageId: 'noNonNull', + line: 1, + column: 2, + suggestions: [ + { + messageId: 'suggestOptionalChain', + output: '!x?.y', + }, + ], + }, + ], + }, + { + code: 'x!.y?.z', + errors: [ + { + messageId: 'noNonNull', + line: 1, + column: 1, + suggestions: [ + { + messageId: 'suggestOptionalChain', + output: 'x?.y?.z', + }, + ], + }, + ], + }, + { + code: 'x![y]', + errors: [ + { + messageId: 'noNonNull', + line: 1, + column: 1, + suggestions: [ + { + messageId: 'suggestOptionalChain', + output: 'x?.[y]', + }, + ], + }, + ], + }, + { + code: 'x![y]?.z', + errors: [ + { + messageId: 'noNonNull', + line: 1, + column: 1, + suggestions: [ + { + messageId: 'suggestOptionalChain', + output: 'x?.[y]?.z', + }, + ], + }, + ], + }, + { + code: 'x.y.z!()', + errors: [ + { + messageId: 'noNonNull', + line: 1, + column: 1, + suggestions: [ + { + messageId: 'suggestOptionalChain', + output: 'x.y.z?.()', + }, + ], + }, + ], + }, + { + code: 'x.y?.z!()', + errors: [ + { + messageId: 'noNonNull', + line: 1, + column: 1, + suggestions: [ + { + messageId: 'suggestOptionalChain', + output: 'x.y?.z?.()', + }, + ], + }, + ], + }, + // some weirder cases that are stupid but valid + { + code: 'x!!!', + errors: [ + { + messageId: 'noNonNull', + line: 1, + column: 1, + endColumn: 5, + suggestions: undefined, + }, + { + messageId: 'noNonNull', + line: 1, + column: 1, + endColumn: 4, + suggestions: undefined, + }, + { + messageId: 'noNonNull', + line: 1, + column: 1, + endColumn: 3, + suggestions: undefined, + }, + ], + }, + { + code: 'x!!.y', + errors: [ + { + messageId: 'noNonNull', + line: 1, + column: 1, + endColumn: 4, + suggestions: [ + { + messageId: 'suggestOptionalChain', + output: 'x!?.y', + }, + ], + }, + { + messageId: 'noNonNull', + line: 1, + column: 1, + endColumn: 3, + suggestions: undefined, + }, + ], + }, + { + code: 'x.y!!', + errors: [ + { + messageId: 'noNonNull', + line: 1, + column: 1, + endColumn: 6, + suggestions: undefined, + }, + { + messageId: 'noNonNull', + line: 1, + column: 1, + endColumn: 5, + suggestions: undefined, + }, + ], + }, + { + code: 'x.y.z!!()', + errors: [ + { + messageId: 'noNonNull', + line: 1, + column: 1, + endColumn: 8, + suggestions: [ + { + messageId: 'suggestOptionalChain', + output: 'x.y.z!?.()', + }, + ], + }, + { + messageId: 'noNonNull', + line: 1, + column: 1, + endColumn: 7, + suggestions: undefined, + }, + ], + }, + { + code: 'x!?.[y].z', + errors: [ + { + messageId: 'noNonNull', + line: 1, + column: 1, + suggestions: [ + { + messageId: 'suggestOptionalChain', + output: 'x?.[y].z', + }, + ], + }, + ], + }, + { + code: 'x!?.y.z', + errors: [ + { + messageId: 'noNonNull', + line: 1, + column: 1, + suggestions: [ + { + messageId: 'suggestOptionalChain', + output: 'x?.y.z', + }, + ], + }, + ], + }, + { + code: 'x.y.z!?.()', errors: [ { messageId: 'noNonNull', line: 1, - column: 17, + column: 1, + suggestions: [ + { + messageId: 'suggestOptionalChain', + output: 'x.y.z?.()', + }, + ], }, ], }, diff --git a/packages/experimental-utils/src/ts-eslint/Rule.ts b/packages/experimental-utils/src/ts-eslint/Rule.ts index c74358da5587..fcc0e11ae701 100644 --- a/packages/experimental-utils/src/ts-eslint/Rule.ts +++ b/packages/experimental-utils/src/ts-eslint/Rule.ts @@ -105,9 +105,9 @@ interface RuleFixer { type ReportFixFunction = ( fixer: RuleFixer, ) => null | RuleFix | RuleFix[] | IterableIterator; -type ReportSuggestionArray = Readonly< - ReportDescriptorBase[] ->; +type ReportSuggestionArray = ReportDescriptorBase< + TMessageIds +>[]; interface ReportDescriptorBase { /** @@ -132,7 +132,7 @@ interface ReportDescriptorWithSuggestion /** * 6.7's Suggestions API */ - suggest?: ReportSuggestionArray | null; + suggest?: Readonly> | null; } interface ReportDescriptorNodeOptionalLoc { diff --git a/packages/experimental-utils/src/ts-eslint/RuleTester.ts b/packages/experimental-utils/src/ts-eslint/RuleTester.ts index c59574589b45..ce3fa87096f4 100644 --- a/packages/experimental-utils/src/ts-eslint/RuleTester.ts +++ b/packages/experimental-utils/src/ts-eslint/RuleTester.ts @@ -45,7 +45,7 @@ interface TestCaseError { column?: number; endLine?: number; endColumn?: number; - suggestions?: SuggestionOutput[]; + suggestions?: SuggestionOutput[] | null; } interface RunTests< From 96ef1e7afd429b98c63b3ed3b83cc5ff5be0e194 Mon Sep 17 00:00:00 2001 From: Retsam Date: Sun, 1 Dec 2019 17:38:18 -0500 Subject: [PATCH 14/16] feat(eslint-plugin): [no-unnec-cond] support nullish coalescing (#1148) Co-authored-by: Brad Zacher --- .../src/rules/no-unnecessary-condition.ts | 56 +++++++++++++++++-- .../rules/no-unnecessary-condition.test.ts | 47 +++++++++++++++- 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 420271a23b86..1ee2c2629806 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -31,6 +31,17 @@ const isPossiblyFalsy = (type: ts.Type): boolean => const isPossiblyTruthy = (type: ts.Type): boolean => unionTypeParts(type).some(type => !isFalsyType(type)); +// Nullish utilities +const nullishFlag = ts.TypeFlags.Undefined | ts.TypeFlags.Null; +const isNullishType = (type: ts.Type): boolean => + isTypeFlagSet(type, nullishFlag); + +const isPossiblyNullish = (type: ts.Type): boolean => + unionTypeParts(type).some(isNullishType); + +const isAlwaysNullish = (type: ts.Type): boolean => + unionTypeParts(type).every(isNullishType); + // isLiteralType only covers numbers and strings, this is a more exhaustive check. const isLiteral = (type: ts.Type): boolean => isBooleanLiteralType(type, true) || @@ -51,6 +62,8 @@ export type Options = [ export type MessageId = | 'alwaysTruthy' | 'alwaysFalsy' + | 'neverNullish' + | 'alwaysNullish' | 'literalBooleanExpression' | 'never'; export default createRule({ @@ -81,6 +94,10 @@ export default createRule({ messages: { alwaysTruthy: 'Unnecessary conditional, value is always truthy.', alwaysFalsy: 'Unnecessary conditional, value is always falsy.', + neverNullish: + 'Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined.', + alwaysNullish: + 'Unnecessary conditional, left-hand side of `??` operator is always `null` or `undefined`', literalBooleanExpression: 'Unnecessary conditional, both sides of the expression are literal values', never: 'Unnecessary conditional, value is `never`', @@ -120,12 +137,35 @@ export default createRule({ ) { return; } - if (isTypeFlagSet(type, TypeFlags.Never)) { - context.report({ node, messageId: 'never' }); - } else if (!isPossiblyTruthy(type)) { - context.report({ node, messageId: 'alwaysFalsy' }); - } else if (!isPossiblyFalsy(type)) { - context.report({ node, messageId: 'alwaysTruthy' }); + const messageId = isTypeFlagSet(type, TypeFlags.Never) + ? 'never' + : !isPossiblyTruthy(type) + ? 'alwaysFalsy' + : !isPossiblyFalsy(type) + ? 'alwaysTruthy' + : undefined; + + if (messageId) { + context.report({ node, messageId }); + } + } + + function checkNodeForNullish(node: TSESTree.Node): void { + const type = getNodeType(node); + // Conditional is always necessary if it involves `any` or `unknown` + if (isTypeFlagSet(type, TypeFlags.Any | TypeFlags.Unknown)) { + return; + } + const messageId = isTypeFlagSet(type, TypeFlags.Never) + ? 'never' + : !isPossiblyNullish(type) + ? 'neverNullish' + : isAlwaysNullish(type) + ? 'alwaysNullish' + : undefined; + + if (messageId) { + context.report({ node, messageId }); } } @@ -178,6 +218,10 @@ export default createRule({ function checkLogicalExpressionForUnnecessaryConditionals( node: TSESTree.LogicalExpression, ): void { + if (node.operator === '??') { + checkNodeForNullish(node.left); + return; + } checkNode(node.left); if (!ignoreRhs) { checkNode(node.right); diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts index 30e511ac119d..0fe1326811c0 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -88,7 +88,23 @@ function test(t: T | []) { function test(a: string) { return a === "a" }`, - + // Nullish coalescing operator + ` +function test(a: string | null) { + return a ?? "default"; +}`, + ` +function test(a: string | undefined) { + return a ?? "default"; +}`, + ` +function test(a: string | null | undefined) { + return a ?? "default"; +}`, + ` +function test(a: unknown) { + return a ?? "default"; +}`, // Supports ignoring the RHS { code: ` @@ -187,6 +203,35 @@ if (x === Foo.a) {} `, errors: [ruleError(8, 5, 'literalBooleanExpression')], }, + // Nullish coalescing operator + { + code: ` +function test(a: string) { + return a ?? 'default'; +}`, + errors: [ruleError(3, 10, 'neverNullish')], + }, + { + code: ` +function test(a: string | false) { + return a ?? 'default'; +}`, + errors: [ruleError(3, 10, 'neverNullish')], + }, + { + code: ` +function test(a: null) { + return a ?? 'default'; +}`, + errors: [ruleError(3, 10, 'alwaysNullish')], + }, + { + code: ` +function test(a: never) { + return a ?? 'default'; +}`, + errors: [ruleError(3, 10, 'never')], + }, // Still errors on in the expected locations when ignoring RHS { From 065393b151b40735bfa6e1eee8a5d368b8a8dfeb Mon Sep 17 00:00:00 2001 From: Mahendra R Date: Mon, 2 Dec 2019 23:00:46 +0530 Subject: [PATCH 15/16] docs(eslint-plugin): typo in the configs README (#1295) --- packages/eslint-plugin/src/configs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/configs/README.md b/packages/eslint-plugin/src/configs/README.md index 28747608efb7..48b3f27444ef 100644 --- a/packages/eslint-plugin/src/configs/README.md +++ b/packages/eslint-plugin/src/configs/README.md @@ -8,7 +8,7 @@ TODO when all config is added. ## eslint-recommended -The `eslint-recommended` ruleset is meant to be used after extending `eslint:recommended`. It disables rules that are already checked by the Typescript compiler and enables rules that promote using more the more modern constructs Typescript allows for. +The `eslint-recommended` ruleset is meant to be used after extending `eslint:recommended`. It disables rules that are already checked by the Typescript compiler and enables rules that promote using the more modern constructs Typescript allows for. ```cjson { From 5adb8a2fded0785d8793b78bf96051aa7b60426a Mon Sep 17 00:00:00 2001 From: James Henry Date: Mon, 2 Dec 2019 18:01:37 +0000 Subject: [PATCH 16/16] chore: publish v2.10.0 --- CHANGELOG.md | 27 ++++++++++++++++++++++ lerna.json | 2 +- packages/eslint-plugin-tslint/CHANGELOG.md | 8 +++++++ packages/eslint-plugin-tslint/package.json | 6 ++--- packages/eslint-plugin/CHANGELOG.md | 25 ++++++++++++++++++++ packages/eslint-plugin/package.json | 4 ++-- packages/experimental-utils/CHANGELOG.md | 12 ++++++++++ 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 | 12 ++++++++++ packages/typescript-estree/package.json | 4 ++-- 14 files changed, 115 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f3790b06c31..19ec0dfaae03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,33 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.10.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.9.0...v2.10.0) (2019-12-02) + + +### Bug Fixes + +* **eslint-plugin:** [no-empty-function] add missed node types ([#1271](https://github.com/typescript-eslint/typescript-eslint/issues/1271)) ([e9d44f5](https://github.com/typescript-eslint/typescript-eslint/commit/e9d44f5)) +* **eslint-plugin:** [no-untyped-pub-sig] ignore set return ([#1264](https://github.com/typescript-eslint/typescript-eslint/issues/1264)) ([6daff10](https://github.com/typescript-eslint/typescript-eslint/commit/6daff10)) +* **eslint-plugin:** [no-unused-expressions] ignore directives ([#1285](https://github.com/typescript-eslint/typescript-eslint/issues/1285)) ([ce4c803](https://github.com/typescript-eslint/typescript-eslint/commit/ce4c803)) +* **eslint-plugin:** [prefer-optional-chain] allow $ in identifiers ([c72c3c1](https://github.com/typescript-eslint/typescript-eslint/commit/c72c3c1)) +* **eslint-plugin:** [prefer-optional-chain] handle more cases ([#1261](https://github.com/typescript-eslint/typescript-eslint/issues/1261)) ([57ddba3](https://github.com/typescript-eslint/typescript-eslint/commit/57ddba3)) +* **eslint-plugin:** [return-await] allow Any and Unknown ([#1270](https://github.com/typescript-eslint/typescript-eslint/issues/1270)) ([ebf5e0a](https://github.com/typescript-eslint/typescript-eslint/commit/ebf5e0a)) +* **eslint-plugin:** [strict-bool-expr] allow nullish coalescing ([#1275](https://github.com/typescript-eslint/typescript-eslint/issues/1275)) ([3b39340](https://github.com/typescript-eslint/typescript-eslint/commit/3b39340)) +* **typescript-estree:** make FunctionDeclaration.body non-null ([#1288](https://github.com/typescript-eslint/typescript-eslint/issues/1288)) ([dc73510](https://github.com/typescript-eslint/typescript-eslint/commit/dc73510)) + + +### Features + +* **eslint-plugin:** [no-empty-func] private/protected construct ([#1267](https://github.com/typescript-eslint/typescript-eslint/issues/1267)) ([3b931ac](https://github.com/typescript-eslint/typescript-eslint/commit/3b931ac)) +* **eslint-plugin:** [no-non-null-assert] add suggestion fixer ([#1260](https://github.com/typescript-eslint/typescript-eslint/issues/1260)) ([e350a21](https://github.com/typescript-eslint/typescript-eslint/commit/e350a21)) +* **eslint-plugin:** [no-unnec-cond] support nullish coalescing ([#1148](https://github.com/typescript-eslint/typescript-eslint/issues/1148)) ([96ef1e7](https://github.com/typescript-eslint/typescript-eslint/commit/96ef1e7)) +* **eslint-plugin:** [prefer-null-coal] opt for suggestion fixer ([#1272](https://github.com/typescript-eslint/typescript-eslint/issues/1272)) ([f84eb96](https://github.com/typescript-eslint/typescript-eslint/commit/f84eb96)) +* **experimental-utils:** add isSpaceBetween declaration to Sou… ([#1268](https://github.com/typescript-eslint/typescript-eslint/issues/1268)) ([f83f04b](https://github.com/typescript-eslint/typescript-eslint/commit/f83f04b)) + + + + + # [2.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.8.0...v2.9.0) (2019-11-25) diff --git a/lerna.json b/lerna.json index 3881b978c009..b81dc17b54b1 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.9.0", + "version": "2.10.0", "npmClient": "yarn", "useWorkspaces": true, "stream": true diff --git a/packages/eslint-plugin-tslint/CHANGELOG.md b/packages/eslint-plugin-tslint/CHANGELOG.md index 634660606399..77dd508e5f6a 100644 --- a/packages/eslint-plugin-tslint/CHANGELOG.md +++ b/packages/eslint-plugin-tslint/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.10.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.9.0...v2.10.0) (2019-12-02) + +**Note:** Version bump only for package @typescript-eslint/eslint-plugin-tslint + + + + + # [2.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.8.0...v2.9.0) (2019-11-25) **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 f9011094ab1b..58cc56a13d50 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.9.0", + "version": "2.10.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.9.0", + "@typescript-eslint/experimental-utils": "2.10.0", "lodash.memoize": "^4.1.2" }, "peerDependencies": { @@ -41,6 +41,6 @@ }, "devDependencies": { "@types/lodash.memoize": "^4.1.4", - "@typescript-eslint/parser": "2.9.0" + "@typescript-eslint/parser": "2.10.0" } } diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 8bd348828083..dee39704de43 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -3,6 +3,31 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.10.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.9.0...v2.10.0) (2019-12-02) + + +### Bug Fixes + +* **eslint-plugin:** [no-empty-function] add missed node types ([#1271](https://github.com/typescript-eslint/typescript-eslint/issues/1271)) ([e9d44f5](https://github.com/typescript-eslint/typescript-eslint/commit/e9d44f5)) +* **eslint-plugin:** [no-untyped-pub-sig] ignore set return ([#1264](https://github.com/typescript-eslint/typescript-eslint/issues/1264)) ([6daff10](https://github.com/typescript-eslint/typescript-eslint/commit/6daff10)) +* **eslint-plugin:** [no-unused-expressions] ignore directives ([#1285](https://github.com/typescript-eslint/typescript-eslint/issues/1285)) ([ce4c803](https://github.com/typescript-eslint/typescript-eslint/commit/ce4c803)) +* **eslint-plugin:** [prefer-optional-chain] allow $ in identifiers ([c72c3c1](https://github.com/typescript-eslint/typescript-eslint/commit/c72c3c1)) +* **eslint-plugin:** [prefer-optional-chain] handle more cases ([#1261](https://github.com/typescript-eslint/typescript-eslint/issues/1261)) ([57ddba3](https://github.com/typescript-eslint/typescript-eslint/commit/57ddba3)) +* **eslint-plugin:** [return-await] allow Any and Unknown ([#1270](https://github.com/typescript-eslint/typescript-eslint/issues/1270)) ([ebf5e0a](https://github.com/typescript-eslint/typescript-eslint/commit/ebf5e0a)) +* **eslint-plugin:** [strict-bool-expr] allow nullish coalescing ([#1275](https://github.com/typescript-eslint/typescript-eslint/issues/1275)) ([3b39340](https://github.com/typescript-eslint/typescript-eslint/commit/3b39340)) + + +### Features + +* **eslint-plugin:** [no-empty-func] private/protected construct ([#1267](https://github.com/typescript-eslint/typescript-eslint/issues/1267)) ([3b931ac](https://github.com/typescript-eslint/typescript-eslint/commit/3b931ac)) +* **eslint-plugin:** [no-non-null-assert] add suggestion fixer ([#1260](https://github.com/typescript-eslint/typescript-eslint/issues/1260)) ([e350a21](https://github.com/typescript-eslint/typescript-eslint/commit/e350a21)) +* **eslint-plugin:** [no-unnec-cond] support nullish coalescing ([#1148](https://github.com/typescript-eslint/typescript-eslint/issues/1148)) ([96ef1e7](https://github.com/typescript-eslint/typescript-eslint/commit/96ef1e7)) +* **eslint-plugin:** [prefer-null-coal] opt for suggestion fixer ([#1272](https://github.com/typescript-eslint/typescript-eslint/issues/1272)) ([f84eb96](https://github.com/typescript-eslint/typescript-eslint/commit/f84eb96)) + + + + + # [2.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.8.0...v2.9.0) (2019-11-25) diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 551199641903..94ff71ea8ef7 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin", - "version": "2.9.0", + "version": "2.10.0", "description": "TypeScript plugin for ESLint", "keywords": [ "eslint", @@ -40,7 +40,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "2.9.0", + "@typescript-eslint/experimental-utils": "2.10.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 fcacc1bd5071..236368fb39cc 100644 --- a/packages/experimental-utils/CHANGELOG.md +++ b/packages/experimental-utils/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.10.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.9.0...v2.10.0) (2019-12-02) + + +### Features + +* **eslint-plugin:** [no-non-null-assert] add suggestion fixer ([#1260](https://github.com/typescript-eslint/typescript-eslint/issues/1260)) ([e350a21](https://github.com/typescript-eslint/typescript-eslint/commit/e350a21)) +* **experimental-utils:** add isSpaceBetween declaration to Sou… ([#1268](https://github.com/typescript-eslint/typescript-eslint/issues/1268)) ([f83f04b](https://github.com/typescript-eslint/typescript-eslint/commit/f83f04b)) + + + + + # [2.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.8.0...v2.9.0) (2019-11-25) diff --git a/packages/experimental-utils/package.json b/packages/experimental-utils/package.json index 5960b669d597..310714e14f53 100644 --- a/packages/experimental-utils/package.json +++ b/packages/experimental-utils/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/experimental-utils", - "version": "2.9.0", + "version": "2.10.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.9.0", + "@typescript-eslint/typescript-estree": "2.10.0", "eslint-scope": "^5.0.0" }, "peerDependencies": { diff --git a/packages/parser/CHANGELOG.md b/packages/parser/CHANGELOG.md index 7153621c2f2f..7b665de26a05 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.10.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.9.0...v2.10.0) (2019-12-02) + +**Note:** Version bump only for package @typescript-eslint/parser + + + + + # [2.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.8.0...v2.9.0) (2019-11-25) **Note:** Version bump only for package @typescript-eslint/parser diff --git a/packages/parser/package.json b/packages/parser/package.json index 41b4ee671951..8641c433f05f 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/parser", - "version": "2.9.0", + "version": "2.10.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.9.0", - "@typescript-eslint/typescript-estree": "2.9.0", + "@typescript-eslint/experimental-utils": "2.10.0", + "@typescript-eslint/typescript-estree": "2.10.0", "eslint-visitor-keys": "^1.1.0" }, "devDependencies": { "@types/glob": "^7.1.1", - "@typescript-eslint/shared-fixtures": "2.9.0", + "@typescript-eslint/shared-fixtures": "2.10.0", "glob": "*" } } diff --git a/packages/shared-fixtures/CHANGELOG.md b/packages/shared-fixtures/CHANGELOG.md index 32efd0513156..cc0c786f2eab 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.10.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.9.0...v2.10.0) (2019-12-02) + +**Note:** Version bump only for package @typescript-eslint/shared-fixtures + + + + + # [2.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.8.0...v2.9.0) (2019-11-25) **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 d7e303ae1a6b..8c2d9ed8c1ed 100644 --- a/packages/shared-fixtures/package.json +++ b/packages/shared-fixtures/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/shared-fixtures", - "version": "2.9.0", + "version": "2.10.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 27973aa5246d..6a4367f1718f 100644 --- a/packages/typescript-estree/CHANGELOG.md +++ b/packages/typescript-estree/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.10.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.9.0...v2.10.0) (2019-12-02) + + +### Bug Fixes + +* **eslint-plugin:** [no-unused-expressions] ignore directives ([#1285](https://github.com/typescript-eslint/typescript-eslint/issues/1285)) ([ce4c803](https://github.com/typescript-eslint/typescript-eslint/commit/ce4c803)) +* **typescript-estree:** make FunctionDeclaration.body non-null ([#1288](https://github.com/typescript-eslint/typescript-eslint/issues/1288)) ([dc73510](https://github.com/typescript-eslint/typescript-eslint/commit/dc73510)) + + + + + # [2.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.8.0...v2.9.0) (2019-11-25) diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index 8a0857d19af5..ced5d06d426a 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/typescript-estree", - "version": "2.9.0", + "version": "2.10.0", "description": "A parser that converts TypeScript source code into an ESTree compatible form", "main": "dist/parser.js", "types": "dist/parser.d.ts", @@ -59,7 +59,7 @@ "@types/lodash.unescape": "^4.0.4", "@types/semver": "^6.2.0", "@types/tmp": "^0.1.0", - "@typescript-eslint/shared-fixtures": "2.9.0", + "@typescript-eslint/shared-fixtures": "2.10.0", "babel-code-frame": "^6.26.0", "lodash.isplainobject": "4.0.6", "tmp": "^0.1.0",