Merge pull request #25372 from Microsoft/fixAddMissingMember_all_dedup

fixAddMissingMember: Improve deduplication in code-fix-all
This commit is contained in:
Mohamed Hegazy 2018-07-02 16:44:58 -07:00 committed by GitHub
commit 18d8ad120c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 165 additions and 39 deletions

View file

@ -71,7 +71,7 @@ namespace ts {
return fixIdToRegistration.get(cast(context.fixId, isString))!.getAllCodeActions!(context);
}
function createCombinedCodeActions(changes: FileTextChanges[], commands?: CodeActionCommand[]): CombinedCodeActions {
export function createCombinedCodeActions(changes: FileTextChanges[], commands?: CodeActionCommand[]): CombinedCodeActions {
return { changes, commands };
}
@ -89,7 +89,7 @@ namespace ts {
return createCombinedCodeActions(changes, commands.length === 0 ? undefined : commands);
}
function eachDiagnostic({ program, sourceFile, cancellationToken }: CodeFixAllContext, errorCodes: number[], cb: (diag: DiagnosticWithLocation) => void): void {
export function eachDiagnostic({ program, sourceFile, cancellationToken }: CodeFixAllContext, errorCodes: number[], cb: (diag: DiagnosticWithLocation) => void): void {
for (const diag of program.getSemanticDiagnostics(sourceFile, cancellationToken).concat(computeSuggestionDiagnostics(sourceFile, program, cancellationToken))) {
if (contains(errorCodes, diag.code)) {
cb(diag as DiagnosticWithLocation);

View file

@ -13,55 +13,103 @@ namespace ts.codefix {
if (!info) return undefined;
if (info.kind === InfoKind.enum) {
const { token, enumDeclaration } = info;
const changes = textChanges.ChangeTracker.with(context, t => addEnumMemberDeclaration(t, context.program.getTypeChecker(), token, enumDeclaration));
const { token, parentDeclaration } = info;
const changes = textChanges.ChangeTracker.with(context, t => addEnumMemberDeclaration(t, context.program.getTypeChecker(), token, parentDeclaration));
return [createCodeFixAction(fixName, changes, [Diagnostics.Add_missing_enum_member_0, token.text], fixId, Diagnostics.Add_all_missing_members)];
}
const { classDeclaration, classDeclarationSourceFile, inJs, makeStatic, token, call } = info;
const methodCodeAction = call && getActionForMethodDeclaration(context, classDeclarationSourceFile, classDeclaration, token, call, makeStatic, inJs, context.preferences);
const { parentDeclaration, classDeclarationSourceFile, inJs, makeStatic, token, call } = info;
const methodCodeAction = call && getActionForMethodDeclaration(context, classDeclarationSourceFile, parentDeclaration, token, call, makeStatic, inJs, context.preferences);
const addMember = inJs ?
singleElementArray(getActionsForAddMissingMemberInJavaScriptFile(context, classDeclarationSourceFile, classDeclaration, token.text, makeStatic)) :
getActionsForAddMissingMemberInTypeScriptFile(context, classDeclarationSourceFile, classDeclaration, token, makeStatic);
singleElementArray(getActionsForAddMissingMemberInJavaScriptFile(context, classDeclarationSourceFile, parentDeclaration, token.text, makeStatic)) :
getActionsForAddMissingMemberInTypeScriptFile(context, classDeclarationSourceFile, parentDeclaration, token, makeStatic);
return concatenate(singleElementArray(methodCodeAction), addMember);
},
fixIds: [fixId],
getAllCodeActions: context => {
const seenNames = createMap<true>();
return codeFixAll(context, errorCodes, (changes, diag) => {
const { program, preferences } = context;
const checker = program.getTypeChecker();
const info = getInfo(diag.file, diag.start, checker);
if (!info || !addToSeen(seenNames, info.token.text)) {
return;
}
const { program, preferences } = context;
const checker = program.getTypeChecker();
const seen = createMap<true>();
if (info.kind === InfoKind.enum) {
const { token, enumDeclaration } = info;
addEnumMemberDeclaration(changes, checker, token, enumDeclaration);
}
else {
const { classDeclaration, classDeclarationSourceFile, inJs, makeStatic, token, call } = info;
// Always prefer to add a method declaration if possible.
if (call) {
addMethodDeclaration(context, changes, classDeclarationSourceFile, classDeclaration, token, call, makeStatic, inJs, preferences);
const classToMembers = new NodeMap<ClassLikeDeclaration, ClassInfo[]>();
return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => {
eachDiagnostic(context, errorCodes, diag => {
const info = getInfo(diag.file, diag.start, checker);
if (!info || !addToSeen(seen, getNodeId(info.parentDeclaration) + "#" + info.token.text)) {
return;
}
if (info.kind === InfoKind.enum) {
const { token, parentDeclaration } = info;
addEnumMemberDeclaration(changes, checker, token, parentDeclaration);
}
else {
if (inJs) {
addMissingMemberInJs(changes, classDeclarationSourceFile, classDeclaration, token.text, makeStatic);
const { parentDeclaration, token } = info;
const infos = classToMembers.getOrUpdate(parentDeclaration, () => []);
if (!infos.some(i => i.token.text === token.text)) infos.push(info);
}
});
classToMembers.forEach((infos, classDeclaration) => {
const superClasses = getAllSuperClasses(classDeclaration, checker);
for (const info of infos) {
// If some superclass added this property, don't add it again.
if (superClasses.some(superClass => {
const superInfos = classToMembers.get(superClass);
return !!superInfos && superInfos.some(({ token }) => token.text === info.token.text);
})) continue;
const { parentDeclaration, classDeclarationSourceFile, inJs, makeStatic, token, call } = info;
// Always prefer to add a method declaration if possible.
if (call) {
addMethodDeclaration(context, changes, classDeclarationSourceFile, parentDeclaration, token, call, makeStatic, inJs, preferences);
}
else {
const typeNode = getTypeNode(program.getTypeChecker(), classDeclaration, token);
addPropertyDeclaration(changes, classDeclarationSourceFile, classDeclaration, token.text, typeNode, makeStatic);
if (inJs) {
addMissingMemberInJs(changes, classDeclarationSourceFile, parentDeclaration, token.text, makeStatic);
}
else {
const typeNode = getTypeNode(program.getTypeChecker(), parentDeclaration, token);
addPropertyDeclaration(changes, classDeclarationSourceFile, parentDeclaration, token.text, typeNode, makeStatic);
}
}
}
}
});
});
}));
},
});
function getAllSuperClasses(cls: ClassLikeDeclaration | undefined, checker: TypeChecker): ReadonlyArray<ClassLikeDeclaration> {
const res: ClassLikeDeclaration[] = [];
while (cls) {
const superElement = getClassExtendsHeritageElement(cls);
const superSymbol = superElement && checker.getSymbolAtLocation(superElement.expression);
const superDecl = superSymbol && find(superSymbol.declarations, isClassLike);
if (superDecl) { res.push(superDecl); }
cls = superDecl;
}
return res;
}
interface InfoBase {
readonly kind: InfoKind;
readonly token: Identifier;
readonly parentDeclaration: EnumDeclaration | ClassLikeDeclaration;
}
enum InfoKind { enum, class }
interface EnumInfo { kind: InfoKind.enum; token: Identifier; enumDeclaration: EnumDeclaration; }
interface ClassInfo { kind: InfoKind.class; token: Identifier; classDeclaration: ClassLikeDeclaration; makeStatic: boolean; classDeclarationSourceFile: SourceFile; inJs: boolean; call: CallExpression | undefined; }
interface EnumInfo extends InfoBase {
readonly kind: InfoKind.enum;
readonly parentDeclaration: EnumDeclaration;
}
interface ClassInfo extends InfoBase {
readonly kind: InfoKind.class;
readonly parentDeclaration: ClassLikeDeclaration;
readonly makeStatic: boolean;
readonly classDeclarationSourceFile: SourceFile;
readonly inJs: boolean;
readonly call: CallExpression | undefined;
}
type Info = EnumInfo | ClassInfo;
function getInfo(tokenSourceFile: SourceFile, tokenPos: number, checker: TypeChecker): Info | undefined {
@ -86,11 +134,11 @@ namespace ts.codefix {
const classDeclarationSourceFile = classDeclaration.getSourceFile();
const inJs = isSourceFileJavaScript(classDeclarationSourceFile);
const call = tryCast(parent.parent, isCallExpression);
return { kind: InfoKind.class, token, classDeclaration, makeStatic, classDeclarationSourceFile, inJs, call };
return { kind: InfoKind.class, token, parentDeclaration: classDeclaration, makeStatic, classDeclarationSourceFile, inJs, call };
}
const enumDeclaration = find(symbol.declarations, isEnumDeclaration);
if (enumDeclaration) {
return { kind: InfoKind.enum, token, enumDeclaration };
return { kind: InfoKind.enum, token, parentDeclaration: enumDeclaration };
}
return undefined;
}

View file

@ -1351,6 +1351,40 @@ namespace ts {
}
}
export interface ReadonlyNodeMap<TNode extends Node, TValue> {
get(node: TNode): TValue | undefined;
has(node: TNode): boolean;
}
export class NodeMap<TNode extends Node, TValue> implements ReadonlyNodeMap<TNode, TValue> {
private map = createMap<{ node: TNode, value: TValue }>();
get(node: TNode): TValue | undefined {
const res = this.map.get(String(getNodeId(node)));
return res && res.value;
}
getOrUpdate(node: TNode, setValue: () => TValue): TValue {
const res = this.get(node);
if (res) return res;
const value = setValue();
this.set(node, value);
return value;
}
set(node: TNode, value: TValue): void {
this.map.set(String(getNodeId(node)), { node, value });
}
has(node: TNode): boolean {
return this.map.has(String(getNodeId(node)));
}
forEach(cb: (value: TValue, node: TNode) => void): void {
this.map.forEach(({ node, value }) => cb(value, node));
}
}
export function getParentNodeInSpan(node: Node | undefined, file: SourceFile, span: TextSpan): Node | undefined {
if (!node) return undefined;

View file

@ -10830,6 +10830,18 @@ declare namespace ts {
forEach(cb: (node: Node) => void): void;
some(pred: (node: Node) => boolean): boolean;
}
interface ReadonlyNodeMap<TNode extends Node, TValue> {
get(node: TNode): TValue | undefined;
has(node: TNode): boolean;
}
class NodeMap<TNode extends Node, TValue> implements ReadonlyNodeMap<TNode, TValue> {
private map;
get(node: TNode): TValue | undefined;
getOrUpdate(node: TNode, setValue: () => TValue): TValue;
set(node: TNode, value: TValue): void;
has(node: TNode): boolean;
forEach(cb: (value: TValue, node: TNode) => void): void;
}
function getParentNodeInSpan(node: Node | undefined, file: SourceFile, span: TextSpan): Node | undefined;
function findModifier(node: Node, kind: Modifier["kind"]): Modifier | undefined;
function insertImport(changes: textChanges.ChangeTracker, sourceFile: SourceFile, importDecl: Statement): void;
@ -11590,8 +11602,10 @@ declare namespace ts {
function getSupportedErrorCodes(): string[];
function getFixes(context: CodeFixContext): CodeFixAction[];
function getAllFixes(context: CodeFixAllContext): CombinedCodeActions;
function createCombinedCodeActions(changes: FileTextChanges[], commands?: CodeActionCommand[]): CombinedCodeActions;
function createFileTextChanges(fileName: string, textChanges: TextChange[]): FileTextChanges;
function codeFixAll(context: CodeFixAllContext, errorCodes: number[], use: (changes: textChanges.ChangeTracker, error: DiagnosticWithLocation, commands: Push<CodeActionCommand>) => void): CombinedCodeActions;
function eachDiagnostic({ program, sourceFile, cancellationToken }: CodeFixAllContext, errorCodes: number[], cb: (diag: DiagnosticWithLocation) => void): void;
}
}
declare namespace ts {

View file

@ -8,8 +8,22 @@
//// }
////}
////
////enum E {}
////E.A;
////class D extends C {}
////class E extends D {
//// method() {
//// this.x = 0;
//// this.ex = 0;
//// }
////}
////
////class Unrelated {
//// method() {
//// this.x = 0;
//// }
////}
////
////enum En {}
////En.A;
verify.codeFixAll({
fixId: "addMissingMember",
@ -27,8 +41,24 @@ verify.codeFixAll({
}
}
enum E {
class D extends C {}
class E extends D {
ex: number;
method() {
this.x = 0;
this.ex = 0;
}
}
class Unrelated {
x: number;
method() {
this.x = 0;
}
}
enum En {
A
}
E.A;`,
En.A;`,
});