Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Code fixes #2888

Merged
merged 32 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ef78a77
Code fixes initial implementation
timotheeguerin Feb 5, 2024
790db01
Apply code fix working
timotheeguerin Feb 5, 2024
6c7d341
better way
timotheeguerin Feb 6, 2024
0ff335e
Basics for language server
timotheeguerin Feb 6, 2024
7193e59
Comment out for now
timotheeguerin Feb 6, 2024
cf6849b
Add quick fix for number -> float64
timotheeguerin Feb 6, 2024
2c88430
alllow on linters
timotheeguerin Feb 12, 2024
524833e
Merge with main
timotheeguerin Feb 12, 2024
40bc8dd
Allow passing codefixes in linter
timotheeguerin Feb 13, 2024
87ca94b
Apply code fix and linter test helper
timotheeguerin Feb 13, 2024
c53efcb
Add codefix testing api
timotheeguerin Feb 14, 2024
cc7d429
Merge branch 'main' of https://github.com/microsoft/typespec into fea…
timotheeguerin Feb 14, 2024
08ddf25
update types and add docs
timotheeguerin Feb 14, 2024
f1df1eb
revert
timotheeguerin Feb 14, 2024
097034a
.
timotheeguerin Feb 14, 2024
050da68
Create feature-code-fixes-2024-1-14-18-23-37.md
timotheeguerin Feb 14, 2024
339294d
add
timotheeguerin Feb 14, 2024
d51dd25
code fixes test
timotheeguerin Feb 14, 2024
daaeb8f
Create feature-code-fixes-2024-1-14-19-34-9.md
timotheeguerin Feb 14, 2024
13a0146
fix
timotheeguerin Feb 14, 2024
dd49d89
Merge branch 'main' of https://github.com/Microsoft/typespec into fea…
timotheeguerin Feb 15, 2024
ebaebb1
Merge branch 'main' into feature/code-fixes
timotheeguerin Feb 15, 2024
d0ab146
Merge branch 'feature/code-fixes' of https://github.com/timotheegueri…
timotheeguerin Feb 15, 2024
8f909bf
format
timotheeguerin Feb 15, 2024
79b8780
Update docs/extending-typespec/codefixes.md
timotheeguerin Mar 1, 2024
da0e355
Update docs/extending-typespec/codefixes.md
timotheeguerin Mar 1, 2024
48e54f0
Update docs/extending-typespec/linters.md
timotheeguerin Mar 1, 2024
99b7e8a
Merge branch 'main' of https://github.com/Microsoft/typespec into fea…
timotheeguerin Mar 1, 2024
477e519
Merge branch 'main' into feature/code-fixes
timotheeguerin Mar 1, 2024
acfa7f8
Fix
timotheeguerin Mar 1, 2024
9362c7f
fix
timotheeguerin Mar 1, 2024
8c89a48
Merge branch 'main' of https://github.com/Microsoft/typespec into fea…
timotheeguerin Mar 5, 2024
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
2 changes: 1 addition & 1 deletion .vscode/launch.json
timotheeguerin marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
"TYPESPEC_DEVELOPMENT_MODE": "true"
},
"presentation": {
"hidden": true
"hidden": true
}
},
{
Expand Down
41 changes: 41 additions & 0 deletions packages/compiler/src/core/code-fixes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type {
CodeFix,
CodeFixContext,
CodeFixEdit,
PrependTextCodeFixEdit,
ReplaceTextCodeFixEdit,
SourceLocation,
} from "./types.js";
import { isArray } from "./util.js";

export async function resolveCodeFix(codeFix: CodeFix): Promise<CodeFixEdit[]> {
const context = createCodeFixContext();
const values = await codeFix.fix(context);
const textEdit = values === undefined ? [] : isArray(values) ? values : [values];
return textEdit;
}

function createCodeFixContext(): CodeFixContext {
return {
prependText,
replaceText,
};

function prependText(node: SourceLocation, text: string): PrependTextCodeFixEdit {
return {
kind: "prepend-text",
pos: node.pos,
text,
file: node.file,
};
}
function replaceText(node: SourceLocation, text: string): ReplaceTextCodeFixEdit {
return {
kind: "replace-text",
pos: node.pos,
end: node.end,
file: node.file,
text,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { isWhiteSpace } from "../charcode.js";
import { defineCodeFix, getSourceLocation } from "../diagnostics.js";
import type { DiagnosticTarget, SourceLocation } from "../types.js";

export function createSuppressCodeFix(diagnosticTarget: DiagnosticTarget, warningCode: string) {
return defineCodeFix({
id: "suppress",
label: `Suppress warning: "${warningCode}"`,
fix: (context) => {
const location = getSourceLocation(diagnosticTarget);
const { lineStart, indent } = findLineStartAndIndent(location);
const updatedLocation = { ...location, pos: lineStart };
return context.prependText(updatedLocation, `${indent}#suppress "${warningCode}" ""\n`);
},
});
}

function findLineStartAndIndent(location: SourceLocation): { lineStart: number; indent: string } {
const text = location.file.text;
let pos = location.pos;
let indent = 0;
while (pos > 0 && text[pos - 1] !== "\n") {
if (isWhiteSpace(text.charCodeAt(pos - 1))) {
indent++;
} else {
indent = 0;
}
pos--;
}
return { lineStart: pos, indent: location.file.text.slice(pos, pos + indent) };
}
5 changes: 5 additions & 0 deletions packages/compiler/src/core/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CharCode } from "./charcode.js";
import { formatLog } from "./logger/index.js";
import type { Program } from "./program.js";
import {
CodeFix,
Diagnostic,
DiagnosticResult,
DiagnosticTarget,
Expand Down Expand Up @@ -386,3 +387,7 @@ export function createDiagnosticCollector(): DiagnosticCollector {
export function ignoreDiagnostics<T>(result: DiagnosticResult<T>): T {
return result[0];
}

export function defineCodeFix(fix: CodeFix): CodeFix {
return fix;
}
6 changes: 6 additions & 0 deletions packages/compiler/src/core/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { validateEncodedNamesConflicts } from "../lib/encoded-names.js";
import { MANIFEST } from "../manifest.js";
import { createBinder } from "./binder.js";
import { Checker, createChecker } from "./checker.js";
import { createSuppressCodeFix } from "./compiler-code-fixes/suppress.codefix.js";
import { compilerAssert, createSourceFile } from "./diagnostics.js";
import {
resolveTypeSpecEntrypoint,
Expand Down Expand Up @@ -1047,6 +1048,11 @@ export async function compile(
return;
}

if (diagnostic.severity === "warning" && diagnostic.target !== NoTarget) {
markcowl marked this conversation as resolved.
Show resolved Hide resolved
mutate(diagnostic).codefixes ??= [];
mutate(diagnostic.codefixes).push(createSuppressCodeFix(diagnostic.target, diagnostic.code));
}

if (options.warningAsError && diagnostic.severity === "warning") {
diagnostic = { ...diagnostic, severity: "error" };
}
Expand Down
27 changes: 27 additions & 0 deletions packages/compiler/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1849,6 +1849,33 @@ export interface Diagnostic {
severity: DiagnosticSeverity;
message: string;
target: DiagnosticTarget | typeof NoTarget;
readonly codefixes?: readonly CodeFix[];
}

export interface CodeFix {
readonly id: string;
readonly label: string;
readonly fix: (fixContext: CodeFixContext) => CodeFixEdit | CodeFixEdit[] | Promise<void> | void;
}

export interface CodeFixContext {
readonly prependText: (location: SourceLocation, text: string) => PrependTextCodeFixEdit;
readonly replaceText: (location: SourceLocation, newText: string) => ReplaceTextCodeFixEdit;
}

export type CodeFixEdit = PrependTextCodeFixEdit | ReplaceTextCodeFixEdit;

export interface PrependTextCodeFixEdit {
readonly kind: "prepend-text";
readonly text: string;
readonly pos: number;
readonly file: SourceFile;
}

export interface ReplaceTextCodeFixEdit extends TextRange {
readonly kind: "replace-text";
readonly text: string;
readonly file: SourceFile;
}

/**
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/server/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ export const serverOptions: CompilerOptions = {
* Time in milliseconds to wait after a file change before recompiling.
*/
export const UPDATE_DEBOUNCE_TIME = 200;

export const Commands = {
APPLY_CODE_FIX: "typespec.applyCodeFix",
};
7 changes: 7 additions & 0 deletions packages/compiler/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { join } from "path";
import { fileURLToPath } from "url";
import { TextDocument } from "vscode-languageserver-textdocument";
import {
ApplyWorkspaceEditParams,
ProposedFeatures,
PublishDiagnosticsParams,
TextDocuments,
WorkspaceEdit,
createConnection,
} from "vscode-languageserver/node.js";
import { NodeHost } from "../core/node-host.js";
Expand Down Expand Up @@ -49,6 +51,9 @@ function main() {
getOpenDocumentByURL(url: string) {
return documents.get(url);
},
applyEdit(paramOrEdit: ApplyWorkspaceEditParams | WorkspaceEdit) {
return connection.workspace.applyEdit(paramOrEdit);
},
};

const s = createServer(host);
Expand Down Expand Up @@ -90,6 +95,8 @@ function main() {
connection.onDocumentHighlight(profile(s.findDocumentHighlight));
connection.onHover(profile(s.getHover));
connection.onSignatureHelp(profile(s.getSignatureHelp));
connection.onCodeAction(profile(s.getCodeActions));
connection.onExecuteCommand(profile(s.executeCommand));
connection.languages.semanticTokens.on(profile(s.buildSemanticTokens));

documents.onDidChangeContent(profile(s.checkChange));
Expand Down
83 changes: 80 additions & 3 deletions packages/compiler/src/server/serverlib.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { TextDocument } from "vscode-languageserver-textdocument";
import {
CodeAction,
CodeActionKind,
CodeActionParams,
CompletionList,
CompletionParams,
DefinitionParams,
Expand All @@ -12,6 +15,7 @@ import {
DocumentHighlightParams,
DocumentSymbol,
DocumentSymbolParams,
ExecuteCommandParams,
FoldingRange,
FoldingRangeParams,
Hover,
Expand Down Expand Up @@ -42,6 +46,7 @@ import {
WorkspaceFoldersChangeEvent,
} from "vscode-languageserver/node.js";
import { CharCode, codePointBefore, isIdentifierContinue } from "../core/charcode.js";
import { resolveCodeFix } from "../core/code-fixes.js";
import { compilerAssert, createSourceFile, getSourceLocation } from "../core/diagnostics.js";
import { formatTypeSpec } from "../core/formatter.js";
import { getTypeName } from "../core/helpers/type-name-utils.js";
Expand All @@ -51,9 +56,12 @@ import { Program } from "../core/program.js";
import { skipTrivia, skipWhiteSpace } from "../core/scanner.js";
import {
AugmentDecoratorStatementNode,
CodeFix,
CodeFixEdit,
CompilerHost,
DecoratorDeclarationStatementNode,
DecoratorExpressionNode,
Diagnostic,
DiagnosticTarget,
IdentifierNode,
Node,
Expand All @@ -67,6 +75,7 @@ import { getNormalizedRealPath, getSourceFileKindFromExt } from "../core/util.js
import { getSemanticTokens } from "./classify.js";
import { createCompileService } from "./compile-service.js";
import { resolveCompletion } from "./completion.js";
import { Commands } from "./constants.js";
import { createFileService } from "./file-service.js";
import { createFileSystemCache } from "./file-system-cache.js";
import { getPositionBeforeTrivia } from "./server-utils.js";
Expand Down Expand Up @@ -104,6 +113,8 @@ export function createServer(host: ServerHost): Server {
serverHost: host,
log,
});
const currentCodeActions = new Map<string, Map<string, [CodeAction, CodeFix]>>();
let codeFixCounter = 0;
compileService.on("compileEnd", (result) => reportDiagnostics(result));

let workspaceFolders: ServerWorkspaceFolder[] = [];
Expand Down Expand Up @@ -137,6 +148,8 @@ export function createServer(host: ServerHost): Server {
getHover,
getSignatureHelp,
getDocumentSymbols,
getCodeActions,
executeCommand,
log,
};

Expand Down Expand Up @@ -172,6 +185,12 @@ export function createServer(host: ServerHost): Server {
triggerCharacters: ["(", ",", "<"],
retriggerCharacters: [")"],
},
codeActionProvider: {
codeActionKinds: ["quickfix"],
},
executeCommandProvider: {
commands: [Commands.APPLY_CODE_FIX],
},
};

if (params.capabilities.workspace?.workspaceFolders) {
Expand Down Expand Up @@ -339,7 +358,7 @@ export function createServer(host: ServerHost): Server {
// as we must send an empty array when a file has no diagnostics or else
// stale diagnostics from a previous run will stick around in the IDE.
//
const diagnosticMap: Map<TextDocument, VSDiagnostic[]> = new Map();
const diagnosticMap: Map<TextDocument, [VSDiagnostic, Diagnostic][]> = new Map();
diagnosticMap.set(document, []);
for (const each of program.sourceFiles.values()) {
const document = (each.file as ServerSourceFile)?.document;
Expand Down Expand Up @@ -380,11 +399,34 @@ export function createServer(host: ServerHost): Server {
diagnostics,
"Diagnostic reported against a source file that was not added to the program."
);
diagnostics.push(diagnostic);
diagnostics.push([diagnostic, each]);
}

for (const [document, diagnostics] of diagnosticMap) {
sendDiagnostics(document, diagnostics);
const documentCodeActions = new Map<string, [CodeAction, CodeFix]>();

for (const [vsDiag, tspDiag] of diagnostics) {
if (tspDiag.codefixes === undefined) {
continue;
}
for (const fix of tspDiag.codefixes) {
const id = `${fix.id}-${codeFixCounter++}`;
const codeAction: CodeAction = {
...CodeAction.create(
fix.label,
{ title: fix.label, command: Commands.APPLY_CODE_FIX, arguments: [document.uri, id] },
CodeActionKind.QuickFix
),
diagnostics: [vsDiag],
};
documentCodeActions.set(id, [codeAction, fix]);
}
}
currentCodeActions.set(document.uri, documentCodeActions);
sendDiagnostics(
document,
diagnostics.map((x) => x[0])
);
}
}

Expand Down Expand Up @@ -734,6 +776,41 @@ export function createServer(host: ServerHost): Server {
return builder.build();
}

async function getCodeActions(params: CodeActionParams): Promise<CodeAction[]> {
const existing = currentCodeActions.get(params.textDocument.uri);
if (!existing) {
return [];
}

return [...existing.values()].map((x) => x[0]);
}

async function executeCommand(params: ExecuteCommandParams) {
if (params.command === Commands.APPLY_CODE_FIX) {
const [documentUri, id] = params.arguments ?? [];
if (documentUri && id) {
const codeFix = currentCodeActions.get(documentUri)?.get(id)?.[1];
console.log("Will apply ", documentUri, id, currentCodeActions.get(documentUri), codeFix);
if (codeFix) {
const edits = await resolveCodeFix(codeFix);
const vsEdits = convertCodeFixEdits(edits);
await host.applyEdit({ changes: { [documentUri]: vsEdits } });
}
}
}
}
function convertCodeFixEdits(edits: CodeFixEdit[]): TextEdit[] {
return edits.map(convertCodeFixEdit);
}
function convertCodeFixEdit(edit: CodeFixEdit): TextEdit {
switch (edit.kind) {
case "prepend-text":
return TextEdit.insert(edit.file.getLineAndCharacterOfPosition(edit.pos), edit.text);
case "replace-text":
return TextEdit.replace(getRange(edit, edit.file), edit.text);
}
}

function documentClosed(change: TextDocumentChangeEvent<TextDocument>) {
// clear diagnostics on file close
sendDiagnostics(change.document, []);
Expand Down
10 changes: 10 additions & 0 deletions packages/compiler/src/server/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import {
ApplyWorkspaceEditParams,
ApplyWorkspaceEditResult,
CodeAction,
CodeActionParams,
CompletionList,
CompletionParams,
DefinitionParams,
Expand All @@ -8,6 +12,7 @@ import {
DocumentHighlightParams,
DocumentSymbol,
DocumentSymbolParams,
ExecuteCommandParams,
FoldingRange,
FoldingRangeParams,
Hover,
Expand Down Expand Up @@ -40,6 +45,9 @@ export interface ServerHost {
readonly getOpenDocumentByURL: (url: string) => TextDocument | undefined;
readonly sendDiagnostics: (params: PublishDiagnosticsParams) => void;
readonly log: (message: string) => void;
readonly applyEdit: (
paramOrEdit: ApplyWorkspaceEditParams | WorkspaceEdit
) => Promise<ApplyWorkspaceEditResult>;
}

export interface CompileResult {
Expand Down Expand Up @@ -71,6 +79,8 @@ export interface Server {
getFoldingRanges(getFoldingRanges: FoldingRangeParams): Promise<FoldingRange[]>;
getDocumentSymbols(params: DocumentSymbolParams): Promise<DocumentSymbol[]>;
documentClosed(change: TextDocumentChangeEvent<TextDocument>): void;
getCodeActions(params: CodeActionParams): Promise<CodeAction[]>;
executeCommand(params: ExecuteCommandParams): Promise<void>;
log(message: string, details?: any): void;
}

Expand Down
Loading
Loading