Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/services/codeFixProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}

Expand All @@ -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);
Expand Down
116 changes: 82 additions & 34 deletions src/services/codefixes/fixAddMissingMember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

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 { program, preferences } = context;
const checker = program.getTypeChecker();
const seen = createMap<true>();

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 {
Expand All @@ -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;
}
Expand Down
34 changes: 34 additions & 0 deletions src/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
14 changes: 14 additions & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
38 changes: 34 additions & 4 deletions tests/cases/fourslash/codeFixAddMissingMember_all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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;`,
});