Avoid crash for import code fixes with dotted require (#47433)

* Add failing test.

* Update failing test.

* Finalized failing test case.

* Separate our usages of utilities that expect variables initialized to require(...) and require(...).foo.

* Renamed types and utilities, removed accidental indentation.

* Renamed both utilitiy functions uniformly.
This commit is contained in:
Daniel Rosenwasser 2022-01-19 15:05:01 -08:00 committed by GitHub
parent 049d4e049f
commit ad5ca673e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 49 additions and 15 deletions

View File

@ -3287,7 +3287,7 @@ namespace ts {
}
if (!isBindingPattern(node.name)) {
if (isInJSFile(node) && isRequireVariableDeclaration(node) && !getJSDocTypeTag(node)) {
if (isInJSFile(node) && isVariableDeclarationInitializedToBareOrAccessedRequire(node) && !getJSDocTypeTag(node)) {
declareSymbolAndAddToSymbolTable(node as Declaration, SymbolFlags.Alias, SymbolFlags.AliasExcludes);
}
else if (isBlockOrCatchScoped(node)) {

View File

@ -2596,7 +2596,7 @@ namespace ts {
&& isAliasableOrJsExpression(node.parent.right)
|| node.kind === SyntaxKind.ShorthandPropertyAssignment
|| node.kind === SyntaxKind.PropertyAssignment && isAliasableOrJsExpression((node as PropertyAssignment).initializer)
|| isRequireVariableDeclaration(node);
|| isVariableDeclarationInitializedToBareOrAccessedRequire(node);
}
function isAliasableOrJsExpression(e: Expression) {
@ -37010,7 +37010,7 @@ namespace ts {
}
// For a commonjs `const x = require`, validate the alias and exit
const symbol = getSymbolOfNode(node);
if (symbol.flags & SymbolFlags.Alias && isRequireVariableDeclaration(node)) {
if (symbol.flags & SymbolFlags.Alias && isVariableDeclarationInitializedToBareOrAccessedRequire(node)) {
checkAliasSymbol(node);
return;
}

View File

@ -4639,7 +4639,7 @@ namespace ts {
export type AnyImportSyntax = ImportDeclaration | ImportEqualsDeclaration;
/* @internal */
export type AnyImportOrRequire = AnyImportSyntax | RequireVariableDeclaration;
export type AnyImportOrRequire = AnyImportSyntax | VariableDeclarationInitializedTo<RequireOrImportCall>;
/* @internal */
export type AnyImportOrRequireStatement = AnyImportSyntax | RequireVariableStatement;
@ -4664,8 +4664,8 @@ namespace ts {
export type RequireOrImportCall = CallExpression & { expression: Identifier, arguments: [StringLiteralLike] };
/* @internal */
export interface RequireVariableDeclaration extends VariableDeclaration {
readonly initializer: RequireOrImportCall;
export interface VariableDeclarationInitializedTo<T extends Expression> extends VariableDeclaration {
readonly initializer: T;
}
/* @internal */
@ -4675,7 +4675,7 @@ namespace ts {
/* @internal */
export interface RequireVariableDeclarationList extends VariableDeclarationList {
readonly declarations: NodeArray<RequireVariableDeclaration>;
readonly declarations: NodeArray<VariableDeclarationInitializedTo<RequireOrImportCall>>;
}
/* @internal */

View File

@ -2074,7 +2074,7 @@ namespace ts {
}
export function getExternalModuleRequireArgument(node: Node) {
return isRequireVariableDeclaration(node) && (getLeftmostAccessExpression(node.initializer) as CallExpression).arguments[0] as StringLiteral;
return isVariableDeclarationInitializedToBareOrAccessedRequire(node) && (getLeftmostAccessExpression(node.initializer) as CallExpression).arguments[0] as StringLiteral;
}
export function isInternalModuleImportEqualsDeclaration(node: Node): node is ImportEqualsDeclaration {
@ -2141,17 +2141,30 @@ namespace ts {
* Returns true if the node is a VariableDeclaration initialized to a require call (see `isRequireCall`).
* This function does not test if the node is in a JavaScript file or not.
*/
export function isRequireVariableDeclaration(node: Node): node is RequireVariableDeclaration {
export function isVariableDeclarationInitializedToRequire(node: Node): node is VariableDeclarationInitializedTo<RequireOrImportCall> {
return isVariableDeclarationInitializedWithRequireHelper(node, /*allowAccessedRequire*/ false);
}
/**
* Like {@link isVariableDeclarationInitializedToRequire} but allows things like `require("...").foo.bar` or `require("...")["baz"]`.
*/
export function isVariableDeclarationInitializedToBareOrAccessedRequire(node: Node): node is VariableDeclarationInitializedTo<RequireOrImportCall | AccessExpression> {
return isVariableDeclarationInitializedWithRequireHelper(node, /*allowAccessedRequire*/ true);
}
function isVariableDeclarationInitializedWithRequireHelper(node: Node, allowAccessedRequire: boolean) {
if (node.kind === SyntaxKind.BindingElement) {
node = node.parent.parent;
}
return isVariableDeclaration(node) && !!node.initializer && isRequireCall(getLeftmostAccessExpression(node.initializer), /*requireStringLiteralLikeArgument*/ true);
return isVariableDeclaration(node) &&
!!node.initializer &&
isRequireCall(allowAccessedRequire ? getLeftmostAccessExpression(node.initializer) : node.initializer, /*requireStringLiteralLikeArgument*/ true);
}
export function isRequireVariableStatement(node: Node): node is RequireVariableStatement {
return isVariableStatement(node)
&& node.declarationList.declarations.length > 0
&& every(node.declarationList.declarations, decl => isRequireVariableDeclaration(decl));
&& every(node.declarationList.declarations, decl => isVariableDeclarationInitializedToRequire(decl));
}
export function isSingleOrDoubleQuote(charCode: number) {

View File

@ -537,7 +537,7 @@ namespace ts.codefix {
const importKind = getImportKind(importingFile, exportKind, compilerOptions);
return mapDefined(importingFile.imports, (moduleSpecifier): FixAddToExistingImportInfo | undefined => {
const i = importFromModuleSpecifier(moduleSpecifier);
if (isRequireVariableDeclaration(i.parent)) {
if (isVariableDeclarationInitializedToRequire(i.parent)) {
return checker.resolveExternalModuleName(moduleSpecifier) === moduleSymbol ? { declaration: i.parent, importKind, symbol, targetFlags } : undefined;
}
if (i.kind === SyntaxKind.ImportDeclaration || i.kind === SyntaxKind.ImportEqualsDeclaration) {

View File

@ -1531,7 +1531,7 @@ namespace ts.FindAllReferences {
// Use the parent symbol if the location is commonjs require syntax on javascript files only.
if (isInJSFile(referenceLocation)
&& referenceLocation.parent.kind === SyntaxKind.BindingElement
&& isRequireVariableDeclaration(referenceLocation.parent)) {
&& isVariableDeclarationInitializedToBareOrAccessedRequire(referenceLocation.parent)) {
referenceSymbol = referenceLocation.parent.symbol;
// The parent will not have a symbol if it's an ObjectBindingPattern (when destructuring is used). In
// this case, just skip it, since the bound identifiers are not an alias of the import.

View File

@ -290,7 +290,7 @@ namespace ts.GoToDefinition {
return declaration.parent.kind === SyntaxKind.NamedImports;
case SyntaxKind.BindingElement:
case SyntaxKind.VariableDeclaration:
return isInJSFile(declaration) && isRequireVariableDeclaration(declaration);
return isInJSFile(declaration) && isVariableDeclarationInitializedToBareOrAccessedRequire(declaration);
default:
return false;
}

View File

@ -621,7 +621,7 @@ namespace ts.FindAllReferences {
Debug.assert((parent as ImportClause | NamespaceImport).name === node);
return true;
case SyntaxKind.BindingElement:
return isInJSFile(node) && isRequireVariableDeclaration(parent);
return isInJSFile(node) && isVariableDeclarationInitializedToBareOrAccessedRequire(parent);
default:
return false;
}

View File

@ -0,0 +1,21 @@
/// <reference path="./fourslash.ts" />
// @module: commonjs
// @checkJs: true
// @Filename: ./library.js
//// module.exports.aaa = function() {}
//// module.exports.bbb = function() {}
// @Filename: ./foo.js
//// var aaa = require("./library.js").aaa;
//// aaa();
//// /*$*/bbb
goTo.marker("$")
verify.codeFixAvailable([
{ description: "Import 'bbb' from module \"./library.js\"" },
{ description: "Ignore this error message" },
{ description: "Disable checking for this file" },
{ description: "Convert to ES module" },
]);