Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(eslint-plugin): [prefer-optional-chain] handling first member expression #2156

Merged
merged 5 commits into from Jun 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/eslint-plugin-tslint/src/rules/config.ts
Expand Up @@ -135,7 +135,7 @@ export default createRule<Options, MessageIds>({
/**
* Format the TSLint results for ESLint
*/
if (result.failures && result.failures.length) {
if (result.failures?.length) {
result.failures.forEach(failure => {
const start = failure.getStartPosition().getLineAndCharacter();
const end = failure.getEndPosition().getLineAndCharacter();
Expand Down
Expand Up @@ -53,7 +53,7 @@ export default util.createRule({
}
case AST_NODE_TYPES.TSDeclareFunction:
case AST_NODE_TYPES.FunctionDeclaration:
return member.id && member.id.name;
return member.id?.name ?? null;
case AST_NODE_TYPES.TSMethodSignature:
return util.getNameFromMember(member, sourceCode);
case AST_NODE_TYPES.TSCallSignatureDeclaration:
Expand Down
2 changes: 1 addition & 1 deletion packages/eslint-plugin/src/rules/array-type.ts
Expand Up @@ -281,7 +281,7 @@ export default util.createRule<Options, MessageIds>({
}

const readonlyPrefix = isReadonlyArrayType ? 'readonly ' : '';
const typeParams = node.typeParameters && node.typeParameters.params;
const typeParams = node.typeParameters?.params;
const messageId =
defaultOption === 'array'
? 'errorStringArray'
Expand Down
Expand Up @@ -537,7 +537,7 @@ export default createRule<Options, MessageIds>({
* A "legal ancestor" is an expression or statement that causes the function to get executed immediately.
* For example, `!(function(){})()` is an outer IIFE even though it is preceded by a ! operator.
*/
let statement = node.parent && node.parent.parent;
let statement = node.parent?.parent;

while (
statement &&
Expand Down
7 changes: 2 additions & 5 deletions packages/eslint-plugin/src/rules/no-empty-function.ts
Expand Up @@ -83,11 +83,8 @@ export default util.createRule<Options, MessageIds>({
function hasParameterProperties(
node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression,
): boolean {
return (
node.params &&
node.params.some(
param => param.type === AST_NODE_TYPES.TSParameterProperty,
)
return node.params?.some(
param => param.type === AST_NODE_TYPES.TSParameterProperty,
);
}

Expand Down
3 changes: 1 addition & 2 deletions packages/eslint-plugin/src/rules/no-use-before-define.ts
Expand Up @@ -137,8 +137,7 @@ function isInInitializer(
return true;
}
if (
node.parent &&
node.parent.parent &&
node.parent?.parent &&
(node.parent.parent.type === AST_NODE_TYPES.ForInStatement ||
node.parent.parent.type === AST_NODE_TYPES.ForOfStatement) &&
isInRange(node.parent.parent.right, location)
Expand Down
6 changes: 4 additions & 2 deletions packages/eslint-plugin/src/rules/prefer-optional-chain.ts
Expand Up @@ -55,18 +55,20 @@ export default util.createRule({
return {
[[
'LogicalExpression[operator="&&"] > Identifier',
'LogicalExpression[operator="&&"] > MemberExpression',
'LogicalExpression[operator="&&"] > BinaryExpression[operator="!=="]',
'LogicalExpression[operator="&&"] > BinaryExpression[operator="!="]',
].join(',')](
yeonjuan marked this conversation as resolved.
Show resolved Hide resolved
initialIdentifierOrNotEqualsExpr:
| TSESTree.BinaryExpression
| TSESTree.Identifier,
| TSESTree.Identifier
| TSESTree.MemberExpression,
): void {
// selector guarantees this cast
const initialExpression = initialIdentifierOrNotEqualsExpr.parent as TSESTree.LogicalExpression;

if (initialExpression.left !== initialIdentifierOrNotEqualsExpr) {
// the identifier is not the deepest left node
// the node(identifier or member expression) is not the deepest left node
return;
}
if (!isValidChainTarget(initialIdentifierOrNotEqualsExpr, true)) {
Expand Down
70 changes: 66 additions & 4 deletions packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts
Expand Up @@ -16,24 +16,44 @@ const baseCases = [
code: 'foo && foo.bar',
output: 'foo?.bar',
},
{
code: 'foo.bar && foo.bar.baz',
output: 'foo.bar?.baz',
},
{
code: 'foo && foo()',
output: 'foo?.()',
},
{
code: 'foo.bar && foo.bar()',
output: 'foo.bar?.()',
},
{
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.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',
},
{
// case where for some reason there is a doubled up expression
code: '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.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',
Expand All @@ -55,10 +75,18 @@ const baseCases = [
output: 'foo?.bar?.baz?.buzz?.()',
},
{
// case with a jump (i.e. a non-nullish prop)
code: '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.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()',
Expand Down Expand Up @@ -94,6 +122,10 @@ const baseCases = [
code: 'foo && foo?.() && foo?.().bar',
output: 'foo?.()?.bar',
},
{
code: 'foo.bar && foo.bar?.() && foo.bar?.().baz',
output: 'foo.bar?.()?.baz',
},
].map(
c =>
({
Expand Down Expand Up @@ -220,8 +252,8 @@ ruleTester.run('prefer-optional-chain', rule, {
},
],
},
// case with inconsistent checks
{
// case with inconsistent checks
code:
'foo && foo.bar != null && foo.bar.baz !== undefined && foo.bar.baz.buzz;',
output: null,
Expand All @@ -237,6 +269,21 @@ ruleTester.run('prefer-optional-chain', rule, {
},
],
},
{
code: noFormat`foo.bar && foo.bar.baz != null && foo.bar.baz.qux !== undefined && foo.bar.baz.qux.buzz;`,
output: null,
errors: [
{
messageId: 'preferOptionalChain',
suggestions: [
{
messageId: 'optionalChainSuggest',
output: 'foo.bar?.baz?.qux?.buzz;',
},
],
},
],
},
// ensure essential whitespace isn't removed
{
code: 'foo && foo.bar(baz => <This Requires Spaces />);',
Expand Down Expand Up @@ -404,6 +451,21 @@ foo?.bar(/* comment */a,
},
],
},
{
code: 'foo.bar && foo.bar?.();',
output: null,
errors: [
{
messageId: 'preferOptionalChain',
suggestions: [
{
messageId: 'optionalChainSuggest',
output: 'foo.bar?.();',
},
],
},
],
},
// using suggestion instead of autofix
{
code:
Expand Down
4 changes: 1 addition & 3 deletions packages/experimental-utils/src/eslint-utils/RuleTester.ts
Expand Up @@ -40,9 +40,7 @@ class RuleTester extends TSESLint.RuleTester {
}
private getFilename(options?: TSESLint.ParserOptions): string {
if (options) {
const filename = `file.ts${
options.ecmaFeatures && options.ecmaFeatures.jsx ? 'x' : ''
}`;
const filename = `file.ts${options.ecmaFeatures?.jsx ? 'x' : ''}`;
if (options.project) {
return path.join(
options.tsconfigRootDir != null
Expand Down
3 changes: 1 addition & 2 deletions packages/parser/src/analyze-scope.ts
Expand Up @@ -875,8 +875,7 @@ export function analyzeScope(
directive: false,
nodejsScope:
parserOptions.sourceType === 'script' &&
(parserOptions.ecmaFeatures &&
parserOptions.ecmaFeatures.globalReturn) === true,
parserOptions.ecmaFeatures?.globalReturn === true,
impliedStrict: false,
sourceType: parserOptions.sourceType,
ecmaVersion: parserOptions.ecmaVersion ?? 2018,
Expand Down
4 changes: 2 additions & 2 deletions packages/typescript-estree/src/convert.ts
Expand Up @@ -375,7 +375,7 @@ export class Converter {
return parameters.map(param => {
const convertedParam = this.convertChild(param) as TSESTree.Parameter;

if (param.decorators && param.decorators.length) {
if (param.decorators?.length) {
convertedParam.decorators = param.decorators.map(el =>
this.convertChild(el),
);
Expand Down Expand Up @@ -1485,7 +1485,7 @@ export class Converter {
);
}

if (superClass.types[0] && superClass.types[0].typeArguments) {
if (superClass.types[0]?.typeArguments) {
result.superTypeParameters = this.convertTypeArgumentsToTypeParameters(
superClass.types[0].typeArguments,
superClass.types[0],
Expand Down
Expand Up @@ -111,7 +111,7 @@ function diagnosticReporter(diagnostic: ts.Diagnostic): void {
*/
function createHash(content: string): string {
// No ts.sys in browser environments.
if (ts.sys && ts.sys.createHash) {
if (ts.sys?.createHash) {
return ts.sys.createHash(content);
}
return content;
Expand Down