Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(language-service): Quick fix to import a component when its sele…
…ctor is used The language service can now generate an import corresponding to a selector. This includes both the TypeScript module import and the decorator import. This applies to both standalone components and components declared in NgModules.
- Loading branch information
Showing
7 changed files
with
297 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
137 changes: 137 additions & 0 deletions
137
packages/language-service/src/codefixes/fix_missing_import.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {ErrorCode as NgCompilerErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics/index'; | ||
import {TemplateTypeChecker} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; | ||
import {findFirstMatchingNode} from '@angular/compiler-cli/src/ngtsc/typecheck/src/comments'; | ||
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST | ||
import ts from 'typescript'; | ||
|
||
import {getTargetAtPosition, TargetNodeKind} from '../template_target'; | ||
import {updateObjectValueForKey} from '../ts_utils'; | ||
|
||
import {CodeActionMeta, convertFileTextChangeInTcb, FixIdForCodeFixesAll} from './utils'; | ||
|
||
const errorCodes: number[] = [ | ||
ngErrorCode(NgCompilerErrorCode.SCHEMA_INVALID_ELEMENT), | ||
]; | ||
|
||
let printer: ts.Printer|null = null; | ||
|
||
function print(node: ts.Node, sourceFile: ts.SourceFile): string { | ||
if (printer === null) printer = ts.createPrinter(); | ||
return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); | ||
} | ||
|
||
function updateImportsForAngularTrait( | ||
checker: TemplateTypeChecker, trait: ts.ClassDeclaration, | ||
importName: string): ts.FileTextChanges|null { | ||
const decorator = checker.getPrimaryAngularDecorator(trait); | ||
if (decorator === null) return null; | ||
const decoratorProps = findFirstMatchingNode(decorator, {filter: ts.isObjectLiteralExpression}); | ||
if (decoratorProps === null) return null; | ||
const appendArrayValFn = (oldValue?: ts.Expression) => { | ||
const newComponentStringLiteral = ts.factory.createIdentifier(importName); | ||
if (!oldValue) return ts.factory.createArrayLiteralExpression([newComponentStringLiteral]); | ||
if (!ts.isArrayLiteralExpression(oldValue)) return oldValue; | ||
if (oldValue.elements.some(e => ts.isIdentifier(e) && e.text === importName)) { | ||
return oldValue; | ||
} | ||
return ts.factory.updateArrayLiteralExpression(oldValue, [ | ||
...oldValue.elements.filter((v) => ts.isIdentifier(v) && v.text !== importName), | ||
ts.factory.createIdentifier(importName), | ||
]); | ||
}; | ||
const newDecoratorProps = updateObjectValueForKey(decoratorProps, 'imports', appendArrayValFn); | ||
return { | ||
fileName: trait.getSourceFile().fileName, | ||
textChanges: [{ | ||
span: { | ||
start: decoratorProps.getStart(), | ||
length: decoratorProps.getEnd() - decoratorProps.getStart() | ||
}, | ||
newText: print(newDecoratorProps, trait.getSourceFile()) | ||
}], | ||
}; | ||
} | ||
|
||
/** | ||
* This code action will generate a new import for an unknown selector. | ||
*/ | ||
export const missingImportMeta: CodeActionMeta = { | ||
errorCodes, | ||
getCodeActions: function( | ||
{templateInfo, start, compiler, formatOptions, preferences, errorCode, tsLs}) { | ||
const checker = compiler.getTemplateTypeChecker(); | ||
|
||
// The error must be an invalid element in tag, which is interpreted as an intended selector. | ||
// TODO: Support unknown selectors in additional positions, such as attributes. | ||
const target = getTargetAtPosition(templateInfo.template, start); | ||
if (target?.context.kind !== TargetNodeKind.ElementInTagContext) return []; | ||
const missingComponentSelector = (target.context.node as any).name as string | undefined; | ||
if (!missingComponentSelector) return []; | ||
|
||
// The class which has an imports array; either a standalone trait or its owning NgModule. | ||
const clazz = checker.getOwningNgModule(templateInfo.component) ?? templateInfo.component; | ||
|
||
// Call type checker APIs to determine the most likely directive for that selector, and the best | ||
// import for it. | ||
// TODO: We can be smarter here about multiple matches. | ||
const allPossibleDirectives = checker.getPotentialTemplateDirectives(templateInfo.component); | ||
const matchingDirectives = | ||
allPossibleDirectives.filter((d) => d.selector === missingComponentSelector); | ||
if (matchingDirectives.length === 0) return []; | ||
const bestMatch = matchingDirectives[0]; | ||
const originalImportedSymbolName = bestMatch.tsSymbol.name; | ||
const potentialImports = checker.getPotentialImportsFor(bestMatch, clazz); | ||
if (potentialImports.length == 0) return []; | ||
const bestPotentialImport = potentialImports[0]; | ||
|
||
// Update the imports at the top of the file. | ||
// TODO: Find the import if already present (possibly qualified). | ||
// TODO: Use AST builder. | ||
const importString = `import { ${bestPotentialImport.symbolName} } from '${ | ||
bestPotentialImport.moduleSpecifier}';\n`; | ||
const fileImportChange = { | ||
fileName: clazz.getSourceFile().fileName, | ||
textChanges: [{span: {start: 0, length: 0}, newText: importString}], | ||
}; | ||
|
||
// Update the imports in the decorator's `imports` array. | ||
const componentImportChange = | ||
updateImportsForAngularTrait(checker, clazz, bestPotentialImport.symbolName); | ||
if (componentImportChange === null) return []; | ||
|
||
// Create the code action to insert the new imports. | ||
const codeActions: ts.CodeFixAction[] = [{ | ||
fixName: FixIdForCodeFixesAll.FIX_MISSING_IMPORT, | ||
description: `Import ${bestPotentialImport.symbolName} from '${ | ||
bestPotentialImport.moduleSpecifier}' on ${clazz.name?.text}`, | ||
changes: [componentImportChange, fileImportChange], | ||
}]; | ||
return codeActions.map(codeAction => { | ||
return { | ||
fixName: codeAction.fixName, | ||
fixId: codeAction.fixId, | ||
fixAllDescription: codeAction.fixAllDescription, | ||
description: codeAction.description, | ||
changes: convertFileTextChangeInTcb(codeAction.changes, compiler), | ||
commands: codeAction.commands, | ||
}; | ||
}); | ||
}, | ||
fixIds: [FixIdForCodeFixesAll.FIX_MISSING_IMPORT], | ||
getAllCodeActions: function( | ||
{tsLs, scope, fixId, formatOptions, preferences, compiler, diagnostics}) { | ||
// 'Fix All' is not supported, because the imports may come from different files and should be | ||
// examined individually. | ||
return { | ||
changes: [], | ||
}; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters