Merge pull request #25372 from Microsoft/fixAddMissingMember_all_dedup
fixAddMissingMember: Improve deduplication in code-fix-all
This commit is contained in:
commit
18d8ad120c
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;`,
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue