From 68c8ac8d2d7ceeafa11f8577470c0250e0725ace Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Wed, 14 Feb 2024 02:32:49 +0900 Subject: [PATCH 1/3] fix(eslint-plugin): [prefer-optional-chan] allow typeof for avoiding reference error --- .../gatherLogicalOperands.ts | 17 ++++++++- .../src/rules/prefer-optional-chain.ts | 1 + .../prefer-optional-chain.test.ts | 37 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts index 73ce6d9255aa..986b2e5f8575 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts @@ -3,6 +3,7 @@ import type { TSESTree, } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import type { SourceCode } from '@typescript-eslint/utils/ts-eslint'; import { isBigIntLiteralType, isBooleanLiteralType, @@ -122,6 +123,7 @@ function isValidFalseBooleanCheckType( export function gatherLogicalOperands( node: TSESTree.LogicalExpression, parserServices: ParserServicesWithTypeInformation, + sourceCode: Readonly, options: PreferOptionalChainOptions, ): { operands: Operand[]; @@ -157,7 +159,20 @@ export function gatherLogicalOperands( comparedExpression.type === AST_NODE_TYPES.UnaryExpression && comparedExpression.operator === 'typeof' ) { - // typeof x === 'undefined' + const argument = comparedExpression.argument; + if (argument.type === AST_NODE_TYPES.Identifier) { + const reference = sourceCode + .getScope(argument) + .references.find(ref => ref.identifier.name === argument.name); + + if (!reference?.resolved?.defs.length) { + // typeof window === 'undefined' + result.push({ type: OperandValidity.Invalid }); + continue; + } + } + + // typeof x.y === 'undefined' result.push({ type: OperandValidity.Valid, comparedName: comparedExpression.argument, diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 24791c5841ed..605045b99fd2 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -179,6 +179,7 @@ export default createRule< const { operands, newlySeenLogicals } = gatherLogicalOperands( node, parserServices, + context.sourceCode, options, ); diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index 852296721a74..cee379d45ac2 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -907,6 +907,7 @@ describe('hand-crafted cases', () => { declare const x: 0n | { a: string }; !x || x.a; `, + "typeof globalThis !== 'undefined' && globalThis.Array();", ], invalid: [ // two errors @@ -1915,6 +1916,42 @@ describe('hand-crafted cases', () => { }, ], }, + { + code: ` + function foo(globalThis?: { Array: Function }) { + typeof globalThis !== 'undefined' && globalThis.Array(); + } + `, + output: ` + function foo(globalThis?: { Array: Function }) { + globalThis?.Array(); + } + `, + errors: [ + { + messageId: 'preferOptionalChain', + }, + ], + }, + { + code: ` + typeof globalThis !== 'undefined' && globalThis.Array && globalThis.Array(); + `, + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: ` + typeof globalThis !== 'undefined' && globalThis.Array?.(); + `, + }, + ], + }, + ], + }, ], }); }); From 9eb4275194e9d2c7808eafd0df4154f46562a10c Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Wed, 21 Feb 2024 22:32:57 +0900 Subject: [PATCH 2/3] test --- packages/type-utils/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/type-utils/package.json b/packages/type-utils/package.json index 9c78d642ce6b..5e01fa49b483 100644 --- a/packages/type-utils/package.json +++ b/packages/type-utils/package.json @@ -40,7 +40,7 @@ "clean": "tsc -b tsconfig.build.json --clean", "postclean": "rimraf dist && rimraf _ts3.4 && rimraf _ts4.3 && rimraf coverage", "format": "prettier --write \"./**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx,json,md,css}\" --ignore-path ../../.prettierignore", - "lint": "npx nx lint", + "lint": "npx nx lin --verbose", "test": "jest --coverage", "typecheck": "tsc -p tsconfig.json --noEmit" }, From 073ac6c304df65a971af15a583ebabc898363c1f Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Thu, 22 Feb 2024 02:00:30 +0900 Subject: [PATCH 3/3] test --- .../src/rules/plugin-test-formatting.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin-internal/src/rules/plugin-test-formatting.ts b/packages/eslint-plugin-internal/src/rules/plugin-test-formatting.ts index 19cb2d33fb1c..843675fd71e7 100644 --- a/packages/eslint-plugin-internal/src/rules/plugin-test-formatting.ts +++ b/packages/eslint-plugin-internal/src/rules/plugin-test-formatting.ts @@ -47,6 +47,7 @@ const a = 1; const prettierConfig = prettier.resolveConfig(__dirname) ?? {}; const START_OF_LINE_WHITESPACE_MATCHER = /^([ ]*)/; +const SEARCH_START_OF_LINE_WHITESPACE_MATCHER = /[^ ]/; const BACKTICK_REGEX = /`/g; const TEMPLATE_EXPR_OPENER = /\$\{/g; @@ -55,16 +56,16 @@ function getExpectedIndentForNode( sourceCodeLines: string[], ): number { const lineIdx = node.loc.start.line - 1; - const indent = START_OF_LINE_WHITESPACE_MATCHER.exec( - sourceCodeLines[lineIdx], - )![1]; - return indent.length; + const indent = sourceCodeLines[lineIdx].search( + SEARCH_START_OF_LINE_WHITESPACE_MATCHER, + ); + if (indent === -1) { + return 0; + } + return indent; } function doIndent(line: string, indent: number): string { - for (let i = 0; i < indent; i += 1) { - line = ' ' + line; - } - return line; + return ' '.repeat(indent) + line; } function getQuote(code: string): '"' | "'" | null {