β¬ οΈ Back to Table of Contents
π analyzeChain¶
π Analysis Summary¶
| Metric | Count |
|---|---|
| π§ Functions | 12 |
| π¦ Imports | 26 |
| π Interfaces | 1 |
| π Type Aliases | 2 |
π Table of Contents¶
π οΈ File Location:¶
π packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts
π¦ Imports¶
| Name | Source |
|---|---|
ParserServicesWithTypeInformation |
@typescript-eslint/utils |
TSESTree |
@typescript-eslint/utils |
ReportDescriptor |
@typescript-eslint/utils/ts-eslint |
ReportFixFunction |
@typescript-eslint/utils/ts-eslint |
RuleContext |
@typescript-eslint/utils/ts-eslint |
SourceCode |
@typescript-eslint/utils/ts-eslint |
AST_NODE_TYPES |
@typescript-eslint/utils |
unionConstituents |
ts-api-utils |
LastChainOperand |
./gatherLogicalOperands |
ValidOperand |
./gatherLogicalOperands |
PreferOptionalChainMessageIds |
./PreferOptionalChainOptions |
PreferOptionalChainOptions |
./PreferOptionalChainOptions |
getFixOrSuggest |
../../util |
getOperatorPrecedenceForNode |
../../util |
isClosingParenToken |
../../util |
isOpeningParenToken |
../../util |
isTypeFlagSet |
../../util |
nullThrows |
../../util |
NullThrowsReasons |
../../util |
OperatorPrecedence |
../../util |
checkNullishAndReport |
./checkNullishAndReport |
compareNodes |
./compareNodes |
NodeComparisonResult |
./compareNodes |
ComparisonType |
./gatherLogicalOperands |
NullishComparisonType |
./gatherLogicalOperands |
Yoda |
./gatherLogicalOperands |
Functions¶
analyzeChain(β¦): void¶
Parameters:
context`RuleContext< PreferOptionalChainMessageIds, [PreferOptionalChainOptions]`
parserServicesParserServicesWithTypeInformationoptionsPreferOptionalChainOptionsnodeTSESTree.NodeoperatorTSESTree.LogicalExpression['operator']chainValidOperand[]lastChainOperandLastChainOperand
Returns: void
Calls:
complex_call_21867subChain.flatsubChainFlat.mapsubChainFlat.slice(0, -1).mapcheckNullishAndReport (from ./checkNullishAndReport)getReportDescriptorsubChain.flat().atanalyzeOperandcompareNodes (from ./compareNodes)maybeReportThenResetsubChain.pushresolveOperandSubsetisValidLastChainOperand
Internal Comments:
// need at least 2 operands in a chain for it to be a chain
/* istanbul ignore next -- previous checks make this unreachable, but keep it for exhaustiveness check */ (x2)
// Things like x !== null && x !== undefined have two nodes, but they are (x2)
// one logical unit here, so we'll allow them to be grouped. (x2)
// we've reached the end of a chain of logical expressions (x3)
// i.e. the current operand doesn't belong to the previous chain. (x3)
// (x6)
// we don't want to throw away the current operand otherwise we will skip it (x3)
// and that can cause us to miss chains. So instead we seed the new chain (x3)
// with the current operand (x3)
// eg this means we can catch cases like: (x3)
// unrelated != null && foo != null && foo.bar != null; (x3)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ first "chain" (x3)
// ^^^^^^^^^^^ newChainSeed (x3)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ second chain (x3)
// TODO - #7170
// check if the name is a superset/equal - if it is, then it likely
// intended to be part of the chain and something we should include in the
// report, eg
// foo == null || foo.bar;
// ^^^^^^^^^^^ valid OR chain
// ^^^^^^^ invalid OR chain logical, but still part of
// the chain for combination purposes
// in case multiple operands were consumed - make sure to correctly increment the index (x3)
// purposely inspect and push the last operand because the prior operands don't matter (x3)
// this also means we won't false-positive in cases like (x3)
// foo !== null && foo !== undefined (x3)
// the operands are comparable, so we can continue searching (x4)
// check the leftovers (x3)
Code
export function analyzeChain(
context: RuleContext<
PreferOptionalChainMessageIds,
[PreferOptionalChainOptions]
>,
parserServices: ParserServicesWithTypeInformation,
options: PreferOptionalChainOptions,
node: TSESTree.Node,
operator: TSESTree.LogicalExpression['operator'],
chain: ValidOperand[],
lastChainOperand?: LastChainOperand,
): void {
// need at least 2 operands in a chain for it to be a chain
if (
chain.length + (lastChainOperand ? 1 : 0) <= 1 ||
/* istanbul ignore next -- previous checks make this unreachable, but keep it for exhaustiveness check */
operator === '??'
) {
return;
}
const analyzeOperand = (() => {
switch (operator) {
case '&&':
return analyzeAndChainOperand;
case '||':
return analyzeOrChainOperand;
}
})();
// Things like x !== null && x !== undefined have two nodes, but they are
// one logical unit here, so we'll allow them to be grouped.
let subChain: (readonly ValidOperand[] | ValidOperand)[] = [];
let lastChain: LastChainOperandForReport | ValidOperand | undefined =
undefined;
const maybeReportThenReset = (
newChainSeed?: readonly [ValidOperand, ...ValidOperand[]],
): void => {
if (subChain.length + (lastChain ? 1 : 0) > 1) {
const subChainFlat = subChain.flat();
const maybeNullishNodes = lastChain
? subChainFlat.map(({ node }) => node)
: subChainFlat.slice(0, -1).map(({ node }) => node);
checkNullishAndReport(
context,
parserServices,
options,
maybeNullishNodes,
getReportDescriptor(
context.sourceCode,
parserServices,
node,
operator,
options,
subChainFlat,
lastChain,
),
);
}
// we've reached the end of a chain of logical expressions
// i.e. the current operand doesn't belong to the previous chain.
//
// we don't want to throw away the current operand otherwise we will skip it
// and that can cause us to miss chains. So instead we seed the new chain
// with the current operand
//
// eg this means we can catch cases like:
// unrelated != null && foo != null && foo.bar != null;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ first "chain"
// ^^^^^^^^^^^ newChainSeed
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ second chain
subChain = newChainSeed ? [newChainSeed] : [];
lastChain = undefined;
};
for (let i = 0; i < chain.length; i += 1) {
const lastOperand = subChain.flat().at(-1);
const operand = chain[i];
const validatedOperands = analyzeOperand(parserServices, operand, i, chain);
if (!validatedOperands) {
// TODO - #7170
// check if the name is a superset/equal - if it is, then it likely
// intended to be part of the chain and something we should include in the
// report, eg
// foo == null || foo.bar;
// ^^^^^^^^^^^ valid OR chain
// ^^^^^^^ invalid OR chain logical, but still part of
// the chain for combination purposes
if (lastOperand) {
const comparisonResult = compareNodes(
lastOperand.comparedName,
operand.comparedName,
);
switch (operand.comparisonType) {
case NullishComparisonType.StrictEqualUndefined:
case NullishComparisonType.NotStrictEqualUndefined: {
if (comparisonResult === NodeComparisonResult.Subset) {
lastChain = operand;
}
break;
}
}
}
maybeReportThenReset();
continue;
}
// in case multiple operands were consumed - make sure to correctly increment the index
i += validatedOperands.length - 1;
const currentOperand = validatedOperands[0];
if (lastOperand) {
const comparisonResult = compareNodes(
lastOperand.comparedName,
// purposely inspect and push the last operand because the prior operands don't matter
// this also means we won't false-positive in cases like
// foo !== null && foo !== undefined
validatedOperands[validatedOperands.length - 1].comparedName,
);
if (comparisonResult === NodeComparisonResult.Subset) {
// the operands are comparable, so we can continue searching
subChain.push(currentOperand);
} else if (comparisonResult === NodeComparisonResult.Invalid) {
maybeReportThenReset(validatedOperands);
} else {
// purposely don't push this case because the node is a no-op and if
// we consider it then we might report on things like
// foo && foo
}
} else {
subChain.push(currentOperand);
}
}
const lastOperand = subChain.flat().at(-1);
if (lastOperand && lastChainOperand) {
const isValidLastChainOperand =
operator === '&&'
? isValidAndLastChainOperand
: isValidOrLastChainOperand;
const { comparedName, comparisonValue, isSubset, isYoda } =
resolveOperandSubset(lastOperand, lastChainOperand);
if (
isSubset &&
isValidLastChainOperand(
comparisonValue,
lastChainOperand.comparisonType,
parserServices,
)
) {
lastChain = {
...lastChainOperand,
comparedName,
comparisonValue,
isYoda,
};
}
}
// check the leftovers
maybeReportThenReset();
}
includesType(parserServices: ParserServicesWithTypeInformation, node: TSESTree.Node, typeFlagIn: ts.TypeFlags): boolean¶
Parameters:
parserServicesParserServicesWithTypeInformationnodeTSESTree.NodetypeFlagInts.TypeFlags
Returns: boolean
Calls:
unionConstituents (from ts-api-utils)parserServices.getTypeAtLocationisTypeFlagSet (from ../../util)
Code
function includesType(
parserServices: ParserServicesWithTypeInformation,
node: TSESTree.Node,
typeFlagIn: ts.TypeFlags,
): boolean {
const typeFlag = typeFlagIn | ts.TypeFlags.Any | ts.TypeFlags.Unknown;
const types = unionConstituents(parserServices.getTypeAtLocation(node));
for (const type of types) {
if (isTypeFlagSet(type, typeFlag)) {
return true;
}
}
return false;
}
isValidAndLastChainOperand(ComparisonValueType: TSESTree.Node, comparisonType: ComparisonType, parserServices: ParserServicesWithTypeInformation): any¶
Parameters:
ComparisonValueTypeTSESTree.NodecomparisonTypeComparisonTypeparserServicesParserServicesWithTypeInformation
Returns: any
Calls:
parserServices.getTypeAtLocationunionConstituents (from ts-api-utils)types.someisTypeFlagSet (from ../../util)types.every
Code
function isValidAndLastChainOperand(
ComparisonValueType: TSESTree.Node,
comparisonType: ComparisonType,
parserServices: ParserServicesWithTypeInformation,
) {
const type = parserServices.getTypeAtLocation(ComparisonValueType);
const ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown;
const types = unionConstituents(type);
switch (comparisonType) {
case ComparisonType.Equal: {
const isNullish = types.some(t =>
isTypeFlagSet(
t,
ANY_UNKNOWN_FLAGS | ts.TypeFlags.Null | ts.TypeFlags.Undefined,
),
);
return !isNullish;
}
case ComparisonType.StrictEqual: {
const isUndefined = types.some(t =>
isTypeFlagSet(t, ANY_UNKNOWN_FLAGS | ts.TypeFlags.Undefined),
);
return !isUndefined;
}
case ComparisonType.NotStrictEqual: {
return types.every(t => isTypeFlagSet(t, ts.TypeFlags.Undefined));
}
case ComparisonType.NotEqual: {
return types.every(t =>
isTypeFlagSet(t, ts.TypeFlags.Undefined | ts.TypeFlags.Null),
);
}
}
}
isValidOrLastChainOperand(ComparisonValueType: TSESTree.Node, comparisonType: ComparisonType, parserServices: ParserServicesWithTypeInformation): any¶
Parameters:
ComparisonValueTypeTSESTree.NodecomparisonTypeComparisonTypeparserServicesParserServicesWithTypeInformation
Returns: any
Calls:
parserServices.getTypeAtLocationunionConstituents (from ts-api-utils)types.someisTypeFlagSet (from ../../util)types.every
Code
function isValidOrLastChainOperand(
ComparisonValueType: TSESTree.Node,
comparisonType: ComparisonType,
parserServices: ParserServicesWithTypeInformation,
) {
const type = parserServices.getTypeAtLocation(ComparisonValueType);
const ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown;
const types = unionConstituents(type);
switch (comparisonType) {
case ComparisonType.NotEqual: {
const isNullish = types.some(t =>
isTypeFlagSet(
t,
ANY_UNKNOWN_FLAGS | ts.TypeFlags.Null | ts.TypeFlags.Undefined,
),
);
return !isNullish;
}
case ComparisonType.NotStrictEqual: {
const isUndefined = types.some(t =>
isTypeFlagSet(t, ANY_UNKNOWN_FLAGS | ts.TypeFlags.Undefined),
);
return !isUndefined;
}
case ComparisonType.Equal:
return types.every(t =>
isTypeFlagSet(t, ts.TypeFlags.Undefined | ts.TypeFlags.Null),
);
case ComparisonType.StrictEqual:
return types.every(t => isTypeFlagSet(t, ts.TypeFlags.Undefined));
}
}
analyzeAndChainOperand(β¦): [ValidOperand] | [ValidOperand, ValidOperand]¶
Parameters:
parserServicesParserServicesWithTypeInformationoperandValidOperandindexnumberchainreadonly ValidOperand[]
Returns: [ValidOperand] | [ValidOperand, ValidOperand]
Calls:
chain.atcompareNodes (from ./compareNodes)includesType
Internal Comments:
// handle `x !== null && x !== undefined` (x2)
// we know the next operand is not an `undefined` check and that this
// operand includes `undefined` - which means that making this an
// optional chain would change the runtime behavior of the expression (x2)
// handle `x !== undefined && x !== null` (x2)
// we know the next operand is not a `null` check and that this
// operand includes `null` - which means that making this an
Code
(
parserServices,
operand,
index,
chain,
) => {
switch (operand.comparisonType) {
case NullishComparisonType.Boolean:
case NullishComparisonType.NotEqualNullOrUndefined:
return [operand];
case NullishComparisonType.NotStrictEqualNull: {
// handle `x !== null && x !== undefined`
const nextOperand = chain.at(index + 1);
if (
nextOperand?.comparisonType ===
NullishComparisonType.NotStrictEqualUndefined &&
compareNodes(operand.comparedName, nextOperand.comparedName) ===
NodeComparisonResult.Equal
) {
return [operand, nextOperand];
}
if (
nextOperand &&
!includesType(
parserServices,
operand.comparedName,
ts.TypeFlags.Undefined,
)
) {
// we know the next operand is not an `undefined` check and that this
// operand includes `undefined` - which means that making this an
// optional chain would change the runtime behavior of the expression
return [operand];
}
return null;
}
case NullishComparisonType.NotStrictEqualUndefined: {
// handle `x !== undefined && x !== null`
const nextOperand = chain.at(index + 1);
if (
nextOperand?.comparisonType ===
NullishComparisonType.NotStrictEqualNull &&
compareNodes(operand.comparedName, nextOperand.comparedName) ===
NodeComparisonResult.Equal
) {
return [operand, nextOperand];
}
if (
includesType(parserServices, operand.comparedName, ts.TypeFlags.Null)
) {
// we know the next operand is not a `null` check and that this
// operand includes `null` - which means that making this an
// optional chain would change the runtime behavior of the expression
return null;
}
return [operand];
}
default:
return null;
}
}
analyzeOrChainOperand(β¦): [ValidOperand] | [ValidOperand, ValidOperand]¶
Parameters:
parserServicesParserServicesWithTypeInformationoperandValidOperandindexnumberchainreadonly ValidOperand[]
Returns: [ValidOperand] | [ValidOperand, ValidOperand]
Calls:
chain.atcompareNodes (from ./compareNodes)includesType
Internal Comments:
// handle `x === null || x === undefined` (x2)
// we know the next operand is not an `undefined` check and that this
// operand includes `undefined` - which means that making this an
// optional chain would change the runtime behavior of the expression (x2)
// handle `x === undefined || x === null` (x2)
// we know the next operand is not a `null` check and that this
// operand includes `null` - which means that making this an
Code
(
parserServices,
operand,
index,
chain,
) => {
switch (operand.comparisonType) {
case NullishComparisonType.NotBoolean:
case NullishComparisonType.EqualNullOrUndefined:
return [operand];
case NullishComparisonType.StrictEqualNull: {
// handle `x === null || x === undefined`
const nextOperand = chain.at(index + 1);
if (
nextOperand?.comparisonType ===
NullishComparisonType.StrictEqualUndefined &&
compareNodes(operand.comparedName, nextOperand.comparedName) ===
NodeComparisonResult.Equal
) {
return [operand, nextOperand];
}
if (
includesType(
parserServices,
operand.comparedName,
ts.TypeFlags.Undefined,
)
) {
// we know the next operand is not an `undefined` check and that this
// operand includes `undefined` - which means that making this an
// optional chain would change the runtime behavior of the expression
return null;
}
return [operand];
}
case NullishComparisonType.StrictEqualUndefined: {
// handle `x === undefined || x === null`
const nextOperand = chain.at(index + 1);
if (
nextOperand?.comparisonType === NullishComparisonType.StrictEqualNull &&
compareNodes(operand.comparedName, nextOperand.comparedName) ===
NodeComparisonResult.Equal
) {
return [operand, nextOperand];
}
if (
includesType(parserServices, operand.comparedName, ts.TypeFlags.Null)
) {
// we know the next operand is not a `null` check and that this
// operand includes `null` - which means that making this an
// optional chain would change the runtime behavior of the expression
return null;
}
return [operand];
}
default:
return null;
}
}
resolveOperandSubset(previousOperand: ValidOperand, lastChainOperand: LastChainOperand): { comparedName: TSESTree.Node; comparisonValue: TSESTree.No⦶
Parameters:
previousOperandValidOperandlastChainOperandLastChainOperand
Returns: { comparedName: TSESTree.Node; comparisonValue: TSESTree.Node; isSubset: boolean; isYoda: boolean; }
Calls:
compareNodes (from ./compareNodes)
Code
(
previousOperand: ValidOperand,
lastChainOperand: LastChainOperand,
) => {
const isNameSubset =
compareNodes(
previousOperand.comparedName,
lastChainOperand.comparedName,
) === NodeComparisonResult.Subset;
if (lastChainOperand.yoda !== Yoda.Unknown) {
return {
comparedName: lastChainOperand.comparedName,
comparisonValue: lastChainOperand.comparisonValue,
isSubset: isNameSubset,
isYoda: lastChainOperand.yoda === Yoda.Yes,
};
}
const isValueSubset =
compareNodes(
previousOperand.comparedName,
lastChainOperand.comparisonValue,
) === NodeComparisonResult.Subset;
if (isNameSubset && !isValueSubset) {
return {
comparedName: lastChainOperand.comparedName,
comparisonValue: lastChainOperand.comparisonValue,
isSubset: true,
isYoda: false,
};
}
if (!isNameSubset && isValueSubset) {
return {
comparedName: lastChainOperand.comparisonValue,
comparisonValue: lastChainOperand.comparedName,
isSubset: true,
isYoda: true,
};
}
return {
comparedName: lastChainOperand.comparisonValue,
comparisonValue: lastChainOperand.comparisonValue,
isSubset: false,
isYoda: true,
};
}
getReportRange(chain: { node: TSESTree.Expression }[], boundary: TSESTree.Range, sourceCode: SourceCode): TSESTree.Range¶
Returns the range that needs to be reported from the chain.
Parameters:
chainany: The chain of logical expressions.boundaryany: The boundary range that the range to report cannot fall outside.sourceCodeany: The source code to get tokens.
Returns: undefined
The range to report.
Raw JSDoc
Calls:
nullThrows (from ../../util)sourceCode.getFirstTokenNullThrowsReasons.MissingTokensourceCode.getLastTokensourceCode.getTokenBeforeisOpeningParenToken (from ../../util)sourceCode.getTokenAfterisClosingParenToken (from ../../util)
Code
function getReportRange(
chain: { node: TSESTree.Expression }[],
boundary: TSESTree.Range,
sourceCode: SourceCode,
): TSESTree.Range {
const leftNode = chain[0].node;
const rightNode = chain[chain.length - 1].node;
let leftMost = nullThrows(
sourceCode.getFirstToken(leftNode),
NullThrowsReasons.MissingToken('any token', leftNode.type),
);
let rightMost = nullThrows(
sourceCode.getLastToken(rightNode),
NullThrowsReasons.MissingToken('any token', rightNode.type),
);
while (leftMost.range[0] > boundary[0]) {
const token = sourceCode.getTokenBefore(leftMost);
if (!token || !isOpeningParenToken(token) || token.range[0] < boundary[0]) {
break;
}
leftMost = token;
}
while (rightMost.range[1] < boundary[1]) {
const token = sourceCode.getTokenAfter(rightMost);
if (!token || !isClosingParenToken(token) || token.range[1] > boundary[1]) {
break;
}
rightMost = token;
}
return [leftMost.range[0], rightMost.range[1]];
}
getReportDescriptor(β¦): ReportDescriptor<PreferOptionalChainMessageIds>¶
Parameters:
sourceCodeSourceCodeparserServicesParserServicesWithTypeInformationnodeTSESTree.Nodeoperator'&&' | '||'optionsPreferOptionalChainOptionssubChainValidOperand[]lastChain(LastChainOperandForReport | ValidOperand) | undefined
Returns: ReportDescriptor<PreferOptionalChainMessageIds>
Calls:
includesTypeflattenChainExpressionnextOperand.sliceparts.pushparts .map(part => { let str = ''; if (part.optional) { str += '?.'; } else { if (part.nonNull) { str += '!'; } if (part.requiresDot) { str += '.'; } } if ( part.precedence !== OperatorPrecedence.Invalid && part.precedence < OperatorPrecedence.Member ) { str +=(${part.text}); } else { str += part.text; } return str; }) .joincomplex_call_15594sourceCode.getTextgetReportRangesourceCode.getTokensisOpeningParenToken (from ../../util)isClosingParenToken (from ../../util)openParensOutsideRange.pushopenParensOutsideRange.popunmatchedCloseParens.pushunmatchedCloseParens.reverseleftCode.slicefixer.replaceTextRangesourceCode.getLocFromIndexgetFixOrSuggest (from ../../util)complex_call_18900nullThrows (from ../../util)sourceCode.getLastTokenNullThrowsReasons.MissingTokensourceCode.getFirstTokenBetweensourceCode.text.substringcomplex_call_19593getOperatorPrecedenceForNode (from ../../util)
Internal Comments:
// user has opted-in to the unsafe behavior (x3)
// we know the last operand is an equality check - so the change in types (x3)
// DOES NOT matter and will not change the runtime result or cause a type (x3)
// check error (x3)
// In its most naive form we could just slap `?.` for every single part of the (x2)
// chain. However this would be undesirable because it'd create unnecessary (x2)
// conditions in the user's code where there were none before - and it would (x2)
// cause errors with rules like our `no-unnecessary-condition`. (x2)
// (x6)
// Instead we want to include the minimum number of `?.` required to correctly (x2)
// unify the code into a single chain. Naively you might think that we can (x2)
// just take the final operand add `?.` after the locations from the previous (x2)
// operands - however this won't be correct either because earlier operands (x2)
// can include a necessary `?.` that's not needed or included in a later (x2)
// operand. (x2)
// So instead what we need to do is to start at the first operand and (x2)
// iteratively diff it against the next operand, and add the difference to the (x2)
// first operand. (x2)
// eg (x2)
// `foo && foo.bar && foo.bar.baz?.bam && foo.bar.baz.bam()` (x2)
// 1) `foo` (x2)
// 2) diff(`foo`, `foo.bar`) = `.bar` (x2)
// 3) result = `foo?.bar` (x2)
// 4) diff(`foo.bar`, `foo.bar.baz?.bam`) = `.baz?.bam` (x2)
// 5) result = `foo?.bar?.baz?.bam` (x2)
// 6) diff(`foo.bar.baz?.bam`, `foo.bar.baz.bam()`) = `()` (x2)
// 7) result = `foo?.bar?.baz?.bam?.()` (x2)
// we need to make the first operand of the diff optional so it matches the (x5)
// logic before merging (x5)
// foo.bar && foo.bar.baz (x5)
// diff = .baz (x5)
// result = foo.bar?.baz (x5)
// retain the ending comparison for cases like (x2)
// x && x.a != null (x2)
// x && typeof x.a !== 'undefined' (x2)
// no precedence for this (x2)
Code
function getReportDescriptor(
sourceCode: SourceCode,
parserServices: ParserServicesWithTypeInformation,
node: TSESTree.Node,
operator: '&&' | '||',
options: PreferOptionalChainOptions,
subChain: ValidOperand[],
lastChain: (LastChainOperandForReport | ValidOperand) | undefined,
): ReportDescriptor<PreferOptionalChainMessageIds> {
const chain = lastChain ? [...subChain, lastChain] : subChain;
const lastOperand = chain[chain.length - 1];
let useSuggestionFixer: boolean;
if (
options.allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing ===
true
) {
// user has opted-in to the unsafe behavior
useSuggestionFixer = false;
}
// optional chain specifically will union `undefined` into the final type
// so we need to make sure that there is at least one operand that includes
// `undefined`, or else we're going to change the final type - which is
// unsafe and might cause downstream type errors.
else if (lastChain) {
useSuggestionFixer = true;
} else if (
lastOperand.comparisonType === NullishComparisonType.EqualNullOrUndefined ||
lastOperand.comparisonType ===
NullishComparisonType.NotEqualNullOrUndefined ||
lastOperand.comparisonType === NullishComparisonType.StrictEqualUndefined ||
lastOperand.comparisonType ===
NullishComparisonType.NotStrictEqualUndefined ||
(operator === '||' &&
lastOperand.comparisonType === NullishComparisonType.NotBoolean)
) {
// we know the last operand is an equality check - so the change in types
// DOES NOT matter and will not change the runtime result or cause a type
// check error
useSuggestionFixer = false;
} else {
useSuggestionFixer = true;
for (const operand of chain) {
if (includesType(parserServices, operand.node, ts.TypeFlags.Undefined)) {
useSuggestionFixer = false;
break;
}
}
// TODO - we could further reduce the false-positive rate of this check by
// checking for cases where the change in types don't matter like
// the test location of an if/while/etc statement.
// but it's quite complex to do this without false-negatives, so
// for now we'll just be over-eager with our matching.
//
// it's MUCH better to false-positive here and only provide a
// suggestion fixer, rather than false-negative and autofix to
// broken code.
}
// In its most naive form we could just slap `?.` for every single part of the
// chain. However this would be undesirable because it'd create unnecessary
// conditions in the user's code where there were none before - and it would
// cause errors with rules like our `no-unnecessary-condition`.
//
// Instead we want to include the minimum number of `?.` required to correctly
// unify the code into a single chain. Naively you might think that we can
// just take the final operand add `?.` after the locations from the previous
// operands - however this won't be correct either because earlier operands
// can include a necessary `?.` that's not needed or included in a later
// operand.
//
// So instead what we need to do is to start at the first operand and
// iteratively diff it against the next operand, and add the difference to the
// first operand.
//
// eg
// `foo && foo.bar && foo.bar.baz?.bam && foo.bar.baz.bam()`
// 1) `foo`
// 2) diff(`foo`, `foo.bar`) = `.bar`
// 3) result = `foo?.bar`
// 4) diff(`foo.bar`, `foo.bar.baz?.bam`) = `.baz?.bam`
// 5) result = `foo?.bar?.baz?.bam`
// 6) diff(`foo.bar.baz?.bam`, `foo.bar.baz.bam()`) = `()`
// 7) result = `foo?.bar?.baz?.bam?.()`
const parts = [];
for (const current of chain) {
const nextOperand = flattenChainExpression(
sourceCode,
current.comparedName,
);
const diff = nextOperand.slice(parts.length);
if (diff.length > 0) {
if (parts.length > 0) {
// we need to make the first operand of the diff optional so it matches the
// logic before merging
// foo.bar && foo.bar.baz
// diff = .baz
// result = foo.bar?.baz
diff[0].optional = true;
}
parts.push(...diff);
}
}
let newCode = parts
.map(part => {
let str = '';
if (part.optional) {
str += '?.';
} else {
if (part.nonNull) {
str += '!';
}
if (part.requiresDot) {
str += '.';
}
}
if (
part.precedence !== OperatorPrecedence.Invalid &&
part.precedence < OperatorPrecedence.Member
) {
str += `(${part.text})`;
} else {
str += part.text;
}
return str;
})
.join('');
if (lastOperand.node.type === AST_NODE_TYPES.BinaryExpression) {
// retain the ending comparison for cases like
// x && x.a != null
// x && typeof x.a !== 'undefined'
const operator = lastOperand.node.operator;
const { left, right } = (() => {
if (lastOperand.isYoda) {
const unaryOperator =
lastOperand.node.right.type === AST_NODE_TYPES.UnaryExpression
? `${lastOperand.node.right.operator} `
: '';
return {
left: sourceCode.getText(lastOperand.node.left),
right: unaryOperator + newCode,
};
}
const unaryOperator =
lastOperand.node.left.type === AST_NODE_TYPES.UnaryExpression
? `${lastOperand.node.left.operator} `
: '';
return {
left: unaryOperator + newCode,
right: sourceCode.getText(lastOperand.node.right),
};
})();
newCode = `${left} ${operator} ${right}`;
} else if (lastOperand.comparisonType === NullishComparisonType.NotBoolean) {
newCode = `!${newCode}`;
}
const reportRange = getReportRange(chain, node.range, sourceCode);
const fix: ReportFixFunction = fixer => {
let unclosedParens = 0;
const tokensInRange = sourceCode.getTokens(node, {
filter: token =>
token.range[0] >= reportRange[0] && token.range[1] <= reportRange[1],
});
for (const token of tokensInRange) {
if (isOpeningParenToken(token)) {
unclosedParens++;
} else if (isClosingParenToken(token)) {
unclosedParens--;
}
}
if (unclosedParens > 0 && reportRange[1] < node.range[1]) {
const openParensOutsideRange: number[] = [];
const unmatchedCloseParens: number[] = [];
const tokensOutRange = sourceCode.getTokens(node, {
filter: token => token.range[1] > reportRange[1],
});
for (const token of tokensOutRange) {
if (isOpeningParenToken(token)) {
openParensOutsideRange.push(token.range[0]);
}
if (isClosingParenToken(token)) {
if (openParensOutsideRange.length > 0) {
openParensOutsideRange.pop();
} else {
unmatchedCloseParens.push(token.range[0]);
}
}
}
let leftCode = sourceCode.getText(node);
unmatchedCloseParens.reverse();
for (const unmatchedParenIndex of unmatchedCloseParens) {
leftCode =
leftCode.slice(0, unmatchedParenIndex) +
leftCode.slice(unmatchedParenIndex + 1);
}
leftCode = leftCode.slice(reportRange[1]);
return fixer.replaceTextRange(node.range, newCode + leftCode);
}
return fixer.replaceTextRange(reportRange, newCode);
};
return {
loc: {
end: sourceCode.getLocFromIndex(reportRange[1]),
start: sourceCode.getLocFromIndex(reportRange[0]),
},
messageId: 'preferOptionalChain',
...getFixOrSuggest({
fixOrSuggest: useSuggestionFixer ? 'suggest' : 'fix',
suggestion: {
fix,
messageId: 'optionalChainSuggest',
},
}),
};
interface FlattenedChain {
nonNull: boolean;
optional: boolean;
precedence: OperatorPrecedence;
requiresDot: boolean;
text: string;
}
function flattenChainExpression(
sourceCode: SourceCode,
node: TSESTree.Node,
): FlattenedChain[] {
switch (node.type) {
case AST_NODE_TYPES.ChainExpression:
return flattenChainExpression(sourceCode, node.expression);
case AST_NODE_TYPES.CallExpression: {
const argumentsText = (() => {
const closingParenToken = nullThrows(
sourceCode.getLastToken(node),
NullThrowsReasons.MissingToken('closing parenthesis', node.type),
);
const openingParenToken = nullThrows(
sourceCode.getFirstTokenBetween(
node.typeArguments ?? node.callee,
closingParenToken,
isOpeningParenToken,
),
NullThrowsReasons.MissingToken('opening parenthesis', node.type),
);
return sourceCode.text.substring(
openingParenToken.range[0],
closingParenToken.range[1],
);
})();
const typeArgumentsText = (() => {
if (node.typeArguments == null) {
return '';
}
return sourceCode.getText(node.typeArguments);
})();
return [
...flattenChainExpression(sourceCode, node.callee),
{
nonNull: false,
optional: node.optional,
// no precedence for this
precedence: OperatorPrecedence.Invalid,
requiresDot: false,
text: typeArgumentsText + argumentsText,
},
];
}
case AST_NODE_TYPES.MemberExpression: {
const propertyText = sourceCode.getText(node.property);
return [
...flattenChainExpression(sourceCode, node.object),
{
nonNull: node.object.type === AST_NODE_TYPES.TSNonNullExpression,
optional: node.optional,
precedence: node.computed
? // computed is already wrapped in [] so no need to wrap in () as well
OperatorPrecedence.Invalid
: getOperatorPrecedenceForNode(node.property),
requiresDot: !node.computed,
text: node.computed ? `[${propertyText}]` : propertyText,
},
];
}
case AST_NODE_TYPES.TSNonNullExpression:
return flattenChainExpression(sourceCode, node.expression);
default:
return [
{
nonNull: false,
optional: false,
precedence: getOperatorPrecedenceForNode(node),
requiresDot: false,
text: sourceCode.getText(node),
},
];
}
}
}
Internal helpers¶
Declared inside another function in this file.
fix(fixer: any): any¶
Parameters:
fixerany
Returns: any
Calls:
sourceCode.getTokensisOpeningParenToken (from ../../util)isClosingParenToken (from ../../util)openParensOutsideRange.pushopenParensOutsideRange.popunmatchedCloseParens.pushsourceCode.getTextunmatchedCloseParens.reverseleftCode.slicefixer.replaceTextRange
Code
fixer => {
let unclosedParens = 0;
const tokensInRange = sourceCode.getTokens(node, {
filter: token =>
token.range[0] >= reportRange[0] && token.range[1] <= reportRange[1],
});
for (const token of tokensInRange) {
if (isOpeningParenToken(token)) {
unclosedParens++;
} else if (isClosingParenToken(token)) {
unclosedParens--;
}
}
if (unclosedParens > 0 && reportRange[1] < node.range[1]) {
const openParensOutsideRange: number[] = [];
const unmatchedCloseParens: number[] = [];
const tokensOutRange = sourceCode.getTokens(node, {
filter: token => token.range[1] > reportRange[1],
});
for (const token of tokensOutRange) {
if (isOpeningParenToken(token)) {
openParensOutsideRange.push(token.range[0]);
}
if (isClosingParenToken(token)) {
if (openParensOutsideRange.length > 0) {
openParensOutsideRange.pop();
} else {
unmatchedCloseParens.push(token.range[0]);
}
}
}
let leftCode = sourceCode.getText(node);
unmatchedCloseParens.reverse();
for (const unmatchedParenIndex of unmatchedCloseParens) {
leftCode =
leftCode.slice(0, unmatchedParenIndex) +
leftCode.slice(unmatchedParenIndex + 1);
}
leftCode = leftCode.slice(reportRange[1]);
return fixer.replaceTextRange(node.range, newCode + leftCode);
}
return fixer.replaceTextRange(reportRange, newCode);
}
flattenChainExpression(sourceCode: SourceCode, node: TSESTree.Node): FlattenedChain[]¶
Parameters:
sourceCodeSourceCodenodeTSESTree.Node
Returns: FlattenedChain[]
Calls:
flattenChainExpressioncomplex_call_18900nullThrows (from ../../util)sourceCode.getLastTokenNullThrowsReasons.MissingTokensourceCode.getFirstTokenBetweensourceCode.text.substringcomplex_call_19593sourceCode.getTextgetOperatorPrecedenceForNode (from ../../util)
Internal Comments:
Code
function flattenChainExpression(
sourceCode: SourceCode,
node: TSESTree.Node,
): FlattenedChain[] {
switch (node.type) {
case AST_NODE_TYPES.ChainExpression:
return flattenChainExpression(sourceCode, node.expression);
case AST_NODE_TYPES.CallExpression: {
const argumentsText = (() => {
const closingParenToken = nullThrows(
sourceCode.getLastToken(node),
NullThrowsReasons.MissingToken('closing parenthesis', node.type),
);
const openingParenToken = nullThrows(
sourceCode.getFirstTokenBetween(
node.typeArguments ?? node.callee,
closingParenToken,
isOpeningParenToken,
),
NullThrowsReasons.MissingToken('opening parenthesis', node.type),
);
return sourceCode.text.substring(
openingParenToken.range[0],
closingParenToken.range[1],
);
})();
const typeArgumentsText = (() => {
if (node.typeArguments == null) {
return '';
}
return sourceCode.getText(node.typeArguments);
})();
return [
...flattenChainExpression(sourceCode, node.callee),
{
nonNull: false,
optional: node.optional,
// no precedence for this
precedence: OperatorPrecedence.Invalid,
requiresDot: false,
text: typeArgumentsText + argumentsText,
},
];
}
case AST_NODE_TYPES.MemberExpression: {
const propertyText = sourceCode.getText(node.property);
return [
...flattenChainExpression(sourceCode, node.object),
{
nonNull: node.object.type === AST_NODE_TYPES.TSNonNullExpression,
optional: node.optional,
precedence: node.computed
? // computed is already wrapped in [] so no need to wrap in () as well
OperatorPrecedence.Invalid
: getOperatorPrecedenceForNode(node.property),
requiresDot: !node.computed,
text: node.computed ? `[${propertyText}]` : propertyText,
},
];
}
case AST_NODE_TYPES.TSNonNullExpression:
return flattenChainExpression(sourceCode, node.expression);
default:
return [
{
nonNull: false,
optional: false,
precedence: getOperatorPrecedenceForNode(node),
requiresDot: false,
text: sourceCode.getText(node),
},
];
}
}
maybeReportThenReset(newChainSeed: readonly [ValidOperand, ...ValidOperandβ¦): void¶
Parameters:
newChainSeedreadonly [ValidOperand, ...ValidOperand[]]
Returns: void
Calls:
subChain.flatsubChainFlat.mapsubChainFlat.slice(0, -1).mapcheckNullishAndReport (from ./checkNullishAndReport)getReportDescriptor
Internal Comments:
// we've reached the end of a chain of logical expressions (x3)
// i.e. the current operand doesn't belong to the previous chain. (x3)
// (x6)
// we don't want to throw away the current operand otherwise we will skip it (x3)
// and that can cause us to miss chains. So instead we seed the new chain (x3)
// with the current operand (x3)
// eg this means we can catch cases like: (x3)
// unrelated != null && foo != null && foo.bar != null; (x3)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ first "chain" (x3)
// ^^^^^^^^^^^ newChainSeed (x3)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ second chain (x3)
Code
(
newChainSeed?: readonly [ValidOperand, ...ValidOperand[]],
): void => {
if (subChain.length + (lastChain ? 1 : 0) > 1) {
const subChainFlat = subChain.flat();
const maybeNullishNodes = lastChain
? subChainFlat.map(({ node }) => node)
: subChainFlat.slice(0, -1).map(({ node }) => node);
checkNullishAndReport(
context,
parserServices,
options,
maybeNullishNodes,
getReportDescriptor(
context.sourceCode,
parserServices,
node,
operator,
options,
subChainFlat,
lastChain,
),
);
}
// we've reached the end of a chain of logical expressions
// i.e. the current operand doesn't belong to the previous chain.
//
// we don't want to throw away the current operand otherwise we will skip it
// and that can cause us to miss chains. So instead we seed the new chain
// with the current operand
//
// eg this means we can catch cases like:
// unrelated != null && foo != null && foo.bar != null;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ first "chain"
// ^^^^^^^^^^^ newChainSeed
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ second chain
subChain = newChainSeed ? [newChainSeed] : [];
lastChain = undefined;
}
Interfaces¶
FlattenedChain¶
Interface Code
Properties¶
| Name | Type | Optional | Description |
|---|---|---|---|
nonNull |
boolean |
β | not shown |
optional |
boolean |
β | not shown |
precedence |
OperatorPrecedence |
β | not shown |
requiresDot |
boolean |
β | not shown |
text |
string |
β | not shown |
Type Aliases¶
LastChainOperandForReport¶
OperandAnalyzer¶
type OperandAnalyzer = (
parserServices: ParserServicesWithTypeInformation,
operand: ValidOperand,
index: number,
chain: readonly ValidOperand[],
) => readonly [ValidOperand, ValidOperand] | readonly [ValidOperand] | null;
Generated by Syntax Scribe