@typespec/compiler
Version:
TypeSpec compiler and standard library
1,092 lines • 61.2 kB
JavaScript
import { CodeActionKind, CompletionList, DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, FileChangeType, MarkupKind, Range, SemanticTokensBuilder, TextDocumentSyncKind, TextEdit, } from "vscode-languageserver";
import { getSymNode } from "../core/binder.js";
import { resolveCodeFix } from "../core/code-fixes.js";
import { compilerAssert, getSourceLocation } from "../core/diagnostics.js";
import { formatTypeSpec } from "../core/formatter.js";
import { getEntityName, getTypeName } from "../core/helpers/type-name-utils.js";
import { builtInLinterRule_UnusedTemplateParameter } from "../core/linter-rules/unused-template-parameter.rule.js";
import { builtInLinterRule_UnusedUsing } from "../core/linter-rules/unused-using.rule.js";
import { builtInLinterLibraryName } from "../core/linter.js";
import { formatLog } from "../core/logger/index.js";
import { getPositionBeforeTrivia } from "../core/parser-utils.js";
import { getNodeAtPosition, getNodeAtPositionDetail, visitChildren } from "../core/parser.js";
import { ensureTrailingDirectorySeparator, getDirectoryPath, joinPaths, normalizePath, resolvePath, } from "../core/path-utils.js";
import { skipTrivia, skipWhiteSpace } from "../core/scanner.js";
import { createSourceFile, getSourceFileKindFromExt } from "../core/source-file.js";
import { createRemoveUnusedSuppressionCodeFix } from "../core/suppression-tracking.js";
import { NoTarget, SyntaxKind, } from "../core/types.js";
import { getTypeSpecCoreTemplates } from "../init/core-templates.js";
import { validateTemplateDefinitions } from "../init/init-template-validate.js";
import { scaffoldNewProject } from "../init/scaffold.js";
import { typespecVersion } from "../manifest.js";
import { resolveModule } from "../module-resolver/index.js";
import { listAllFilesInDir } from "../utils/fs-utils.js";
import { getNormalizedRealPath, resolveTspMain } from "../utils/misc.js";
import { getSemanticTokens } from "./classify.js";
import { createCompileService } from "./compile-service.js";
import { resolveCompletion } from "./completion.js";
import { convertDiagnosticToLsp } from "./diagnostics.js";
import { createFileService } from "./file-service.js";
import { createFileSystemCache } from "./file-system-cache.js";
import { LibraryProvider } from "./lib-provider.js";
import { NpmPackageProvider } from "./npm-package-provider.js";
import { getRenameImportEdit, getUpdatedImportValue } from "./rename-file.js";
import { getSymbolStructure } from "./symbol-structure.js";
import { provideTspconfigCompletionItems } from "./tspconfig/completion.js";
import { getParameterDocumentation, getSymbolDetails, getTemplateParameterDocumentation, } from "./type-details.js";
import { SemanticTokenKind, } from "./types.js";
import { UpdateManager } from "./update-manager.js";
export function createServer(host, clientConfigsProvider) {
const fileService = createFileService({ serverHost: host });
// Cache all file I/O. Only open documents are sent over the LSP pipe. When
// the compiler reads a file that isn't open, we use this cache to avoid
// hitting the disk. Entries are invalidated when LSP client notifies us of
// a file change.
const fileSystemCache = createFileSystemCache({
fileService,
log,
});
const compilerHost = createCompilerHost();
const npmPackageProvider = new NpmPackageProvider(compilerHost);
const emitterProvider = new LibraryProvider(npmPackageProvider, (exports) => exports.$onEmit !== undefined);
const linterProvider = new LibraryProvider(npmPackageProvider, (exports) => exports.$linter !== undefined);
const updateManager = new UpdateManager("doc-update", log);
const signatureHelpUpdateManager = new UpdateManager("signature-help", log, host.getDocumentUpdateDebounceDelay);
const compileService = createCompileService({
fileService,
fileSystemCache,
compilerHost,
serverHost: host,
updateManager,
log,
clientConfigsProvider,
});
let currentDiagnosticIndex = new Map();
let diagnosticIdCounter = 0;
let workspaceFolders = [];
let isInitialized = false;
let pendingMessages = [];
/**
* Wraps an LSP handler to preserve the server-side error details when it crashes.
*
* By default, the JSON-RPC layer (vscode-languageserver) catches handler errors and
* creates a new ResponseError using only `error.message`, discarding the original stack
* trace. On the client side, the telemetry framework then captures this as an unhandled
* error, but the `unhandled_error_stack` only shows the client-side message handling code:
*
* ```
* Error: Request textDocument/hover failed with message: Cannot read properties of undefined (reading 'kind')
* at handleResponse (extension.cjs:2104:40) // <-- client-side LSP message handler
* at handleMessage (extension.cjs:1914:11)
* at processMessageQueue (extension.cjs:1929:13)
* at Immediate.<anonymous> (extension.cjs:1905:11)
* ```
*
* The actual server-side crash location (e.g., in the checker or parser) is completely lost.
*
* This wrapper catches the error first and re-throws a new Error whose message includes
* the full original error details (stack trace for Error instances, String() for others).
* The JSON-RPC layer then forwards this enriched message to the client, so the
* telemetry `unhandled_error_message` will contain the server-side crash location:
*
* ```
* [getHover] TypeError: Cannot read properties of undefined (reading 'kind')
* at Checker.getTypeForNode (checker.ts:1234:15) // <-- actual crash location
* at getHover (serverlib.ts:826:52)
* ...
* ```
*/
function wrapUnhandledError(fn) {
const name = fn.name || "anonymous";
return (async (...args) => {
try {
return await fn(...args);
}
catch (e) {
if (e instanceof Error) {
const detail = e.stack ? `${e.message}\n${e.stack}` : e.message;
throw new Error(`[${name}] ${detail}`, { cause: e });
}
else if (typeof e === "string") {
throw new Error(`[${name}] ${e}`, { cause: e });
}
else if (typeof e === "object" && e !== null) {
let detail;
try {
detail = JSON.stringify(e);
}
catch {
throw e;
}
throw new Error(`[${name}] ${detail}`, { cause: e });
}
throw e;
}
});
}
return {
get pendingMessages() {
return pendingMessages;
},
get workspaceFolders() {
return workspaceFolders;
},
compile: wrapUnhandledError(compile),
initialize: wrapUnhandledError(initialize),
initialized: wrapUnhandledError(initialized),
workspaceFoldersChanged: wrapUnhandledError(workspaceFoldersChanged),
watchedFilesChanged: wrapUnhandledError(watchedFilesChanged),
formatDocument: wrapUnhandledError(formatDocument),
gotoDefinition: wrapUnhandledError(gotoDefinition),
documentClosed: wrapUnhandledError(documentClosed),
documentOpened: wrapUnhandledError(documentOpened),
complete: wrapUnhandledError(complete),
findReferences: wrapUnhandledError(findReferences),
findDocumentHighlight: wrapUnhandledError(findDocumentHighlight),
prepareRename: wrapUnhandledError(prepareRename),
rename: wrapUnhandledError(rename),
renameFiles: wrapUnhandledError(renameFiles),
getSemanticTokens: wrapUnhandledError(getSemanticTokensForDocument),
buildSemanticTokens: wrapUnhandledError(buildSemanticTokens),
checkChange: wrapUnhandledError(checkChange),
getFoldingRanges: wrapUnhandledError(getFoldingRanges),
getHover: wrapUnhandledError(getHover),
getSignatureHelp: wrapUnhandledError(getSignatureHelp),
getDocumentSymbols: wrapUnhandledError(getDocumentSymbols),
getCodeActions: wrapUnhandledError(getCodeActions),
resolveCodeAction: wrapUnhandledError(resolveCodeAction),
log,
reportDiagnostics: wrapUnhandledError(reportDiagnostics),
getInitProjectContext: wrapUnhandledError(getInitProjectContext),
validateInitProjectTemplate: wrapUnhandledError(validateInitProjectTemplate),
initProject: wrapUnhandledError(initProject),
internalCompile: wrapUnhandledError(internalCompile),
};
async function initialize(params) {
const tokenLegend = {
tokenTypes: Object.keys(SemanticTokenKind)
.filter((x) => Number.isNaN(Number(x)))
.map((x) => x.slice(0, 1).toLocaleLowerCase() + x.slice(1)),
tokenModifiers: [],
};
updateManager.setCallback(async (updates) => {
if (updates.length === 0) {
return;
}
// we only need to compile the last update, the previous update will be overwritten by the last one anyway
const lastUpdate = updates.reduce((pre, cur) => {
return cur.latestUpdateTimestamp > pre.latestUpdateTimestamp ? cur : pre;
});
const curVersion = updateManager.docChangedVersion;
const result = await compileService.compile(lastUpdate.latest, undefined, {
mode: "full",
// the callback should be or have been triggered for the new version, so this one should be
// cancelled in this case
isCancelled: () => updateManager.docChangedVersion > curVersion,
});
if (result && curVersion === updateManager.docChangedVersion) {
await reportDiagnostics(result);
}
});
signatureHelpUpdateManager.setCallback(async (_updates, triggeredBy) => {
// for signature help, we should always compile against the document that triggered the request and core mode is enough for us
// debounce can help to avoid the unnecessary triggering and compiler cache should be able to avoid the duplicates compile
return await compileInCoreMode(triggeredBy);
});
const capabilities = {
textDocumentSync: TextDocumentSyncKind.Incremental,
definitionProvider: true,
foldingRangeProvider: true,
hoverProvider: true,
documentSymbolProvider: true,
documentHighlightProvider: true,
completionProvider: {
resolveProvider: false,
triggerCharacters: [".", "@", "/"],
},
semanticTokensProvider: {
full: true,
legend: tokenLegend,
},
referencesProvider: true,
renameProvider: {
prepareProvider: true,
},
documentFormattingProvider: true,
signatureHelpProvider: {
triggerCharacters: ["(", ",", "<"],
retriggerCharacters: [")"],
},
codeActionProvider: {
codeActionKinds: ["quickfix"],
resolveProvider: true,
},
};
if (params.capabilities.workspace?.workspaceFolders) {
for (const w of params.workspaceFolders ?? []) {
workspaceFolders.push({
...w,
path: ensureTrailingDirectorySeparator(await fileService.fileURLToRealPath(w.uri)),
});
}
capabilities.workspace = {
workspaceFolders: {
supported: true,
changeNotifications: true,
},
fileOperations: {
didRename: {
filters: [
{
scheme: "file",
pattern: { glob: "**/*.tsp", matches: "file" },
},
{
scheme: "file",
pattern: { glob: "**/*", matches: "folder" },
},
],
},
},
};
// eslint-disable-next-line @typescript-eslint/no-deprecated
}
else if (params.rootUri) {
workspaceFolders = [
{
name: "<root>",
// eslint-disable-next-line @typescript-eslint/no-deprecated
uri: params.rootUri,
path: ensureTrailingDirectorySeparator(
// eslint-disable-next-line @typescript-eslint/no-deprecated
await fileService.fileURLToRealPath(params.rootUri)),
},
];
// eslint-disable-next-line @typescript-eslint/no-deprecated
}
else if (params.rootPath) {
workspaceFolders = [
{
name: "<root>",
// eslint-disable-next-line @typescript-eslint/no-deprecated
uri: compilerHost.pathToFileURL(params.rootPath),
path: ensureTrailingDirectorySeparator(
// eslint-disable-next-line @typescript-eslint/no-deprecated
await getNormalizedRealPath(compilerHost, params.rootPath)),
},
];
}
log({ level: "info", message: `Workspace Folders`, detail: workspaceFolders });
const customCapacities = {
getInitProjectContext: true,
initProject: true,
validateInitProjectTemplate: true,
internalCompile: true,
};
// the file path is expected to be .../@typespec/compiler/dist/src/server/serverlib.js
const curFile = normalizePath(compilerHost.fileURLToPath(import.meta.url));
const SERVERLIB_PATH_ENDWITH = "/dist/src/server/serverlib.js";
let compilerRootFolder = undefined;
if (!curFile.endsWith(SERVERLIB_PATH_ENDWITH)) {
// Ignore this could be the playground or standalone cli
}
else {
compilerRootFolder = curFile.slice(0, curFile.length - SERVERLIB_PATH_ENDWITH.length);
}
const result = {
serverInfo: {
name: "TypeSpec Language Server",
version: typespecVersion,
},
capabilities,
customCapacities,
compilerRootFolder,
compilerCliJsPath: compilerRootFolder
? joinPaths(compilerRootFolder, "cmd", "tsp.js")
: undefined,
};
return result;
}
function initialized(params) {
updateManager.start();
signatureHelpUpdateManager.start();
isInitialized = true;
log({ level: "info", message: "Initialization complete." });
}
async function getInitProjectContext() {
return {
coreInitTemplates: await getTypeSpecCoreTemplates(host.compilerHost),
};
}
async function compile(document, additionalOptions, serverCompileOptions) {
return compileService.compile(document, additionalOptions, serverCompileOptions);
}
async function compileInCoreMode(document) {
return compile(document, undefined, { mode: "core" });
}
async function validateInitProjectTemplate(param) {
const { template } = param;
// even when the strict validation fails, we still try to proceed with relaxed validation
// so just do relaxed validation directly here
const validationResult = validateTemplateDefinitions(template, NoTarget, false);
if (!validationResult.valid) {
for (const diag of validationResult.diagnostics) {
log({
level: diag.severity,
message: diag.message,
detail: {
code: diag.code,
url: diag.url,
},
});
}
}
return validationResult.valid;
}
async function initProject(param) {
try {
await scaffoldNewProject(compilerHost, param.config);
return true;
}
catch (e) {
log({ level: "error", message: "Unexpected error when initializing project", detail: e });
return false;
}
}
async function internalCompile(param) {
const option = {
...param.options,
};
const result = await compileService.compile(param.doc, option, {
skipCache: true,
skipOldProgramFromCache: true,
mode: "full",
});
if (result === undefined) {
return {
hasError: true,
diagnostics: [
{
code: "internal-error",
message: "Failed to get compiler result, please check the compilation output for details",
severity: "error",
target: NoTarget,
url: undefined,
},
],
entrypoint: undefined,
options: undefined,
};
}
else {
result.tracker.getLogs().forEach((logItem) => host.log(logItem));
return {
hasError: result.program.hasError(),
diagnostics: result.program.diagnostics.map((diagnostic) => {
const target = getSourceLocation(diagnostic.target, { locateId: true });
let position = undefined;
if (target?.file) {
const lineAndCharacter = target.file.getLineAndCharacterOfPosition(target.pos);
position = {
line: lineAndCharacter.line + 1,
column: lineAndCharacter.character + 1,
};
}
return {
code: diagnostic.code,
message: diagnostic.message,
severity: diagnostic.severity,
target: { ...target, position: position },
url: diagnostic.url,
};
}),
entrypoint: result.document?.uri,
options: result.program.compilerOptions,
};
}
}
async function renameFiles(params) {
const firstFilePath = params.files[0];
if (!firstFilePath) {
return;
}
// Update cache for renamed folders.
for (const file of params.files) {
const oldFilePath = await fileService.getPath({ uri: file.oldUri });
const newFilePath = await fileService.getPath({ uri: file.newUri });
const isDirRename = !file.oldUri.endsWith(".tsp");
if (isDirRename) {
const files = await listAllFilesInDir(compilerHost, newFilePath);
for (const file of files) {
const oldFile = resolvePath(oldFilePath, file);
fileSystemCache.notify([
{ uri: fileService.getURL(oldFile), type: FileChangeType.Deleted },
]);
}
}
}
const mainFile = await compileService.getMainFileForDocument(await fileService.getPath({ uri: firstFilePath.newUri }));
if (mainFile === undefined) {
log({ level: "debug", message: `failed to resolve main file for ${firstFilePath.newUri}` });
return;
}
// Add this method to resolve timing issues between renamed files and `fs.stat`
// to prevent `fs.stat` from getting the files before modification.
// Currently the test requires a delay of 300ms
await new Promise((resolve) => setTimeout(resolve, 300));
// There will be no event triggered if the renamed file is not opened in vscode, also even when it's opened
// there will be only closed and opened event triggered for the old and new file url, so send fire the update
// explicitly here to make sure the change is not missed.
void updateManager.scheduleUpdate({ uri: fileService.getURL(mainFile) }, "renamed");
const result = await compileInCoreMode({ uri: fileService.getURL(mainFile) });
if (!result) {
log({
level: "debug",
message: `The main tsp file '${mainFile}' compilation failed, please check the detailed compilation message`,
});
return;
}
for (const file of params.files) {
const oldFilePath = await fileService.getPath({ uri: file.oldUri });
const newFilePath = await fileService.getPath({ uri: file.newUri });
const isDirRename = !file.oldUri.endsWith(".tsp");
for (const diagnostic of result.program.diagnostics) {
if (diagnostic.code !== "import-not-found")
continue;
const target = diagnostic.target;
if (target.kind === SyntaxKind.ImportStatement) {
const result = getUpdatedImportValue(target, {
oldPath: oldFilePath,
newPath: newFilePath,
isDirRename,
});
if (result) {
log({
level: "info",
message: `The imports content '${target.path.value}' needs to be modified in tsp file '${result.filePath}' to '${result.newValue}'`,
});
const edit = getRenameImportEdit(target, result.newValue);
await host.applyEdit({ changes: { [fileService.getURL(result.filePath)]: [edit] } });
}
}
}
}
}
async function workspaceFoldersChanged(e) {
log({ level: "info", message: "Workspace Folders Changed", detail: e });
const map = new Map(workspaceFolders.map((f) => [f.uri, f]));
for (const folder of e.removed) {
map.delete(folder.uri);
}
for (const folder of e.added) {
map.set(folder.uri, {
...folder,
path: ensureTrailingDirectorySeparator(await fileService.fileURLToRealPath(folder.uri)),
});
}
workspaceFolders = Array.from(map.values());
log({ level: "info", message: `Workspace Folders`, detail: workspaceFolders });
}
function watchedFilesChanged(params) {
fileSystemCache.notify(params.changes);
npmPackageProvider.notify(params.changes);
}
function isTspConfigFile(doc) {
return doc.uri.endsWith("tspconfig.yaml");
}
async function getFoldingRanges(params) {
if (isTspConfigFile(params.textDocument))
return [];
const ast = await compileService.getScript(params.textDocument);
if (!ast) {
return [];
}
const file = ast.file;
const ranges = [];
let rangeStartSingleLines = -1;
for (let i = 0; i < ast.comments.length; i++) {
const comment = ast.comments[i];
if (comment.kind === SyntaxKind.LineComment &&
i + 1 < ast.comments.length &&
ast.comments[i + 1].kind === SyntaxKind.LineComment &&
ast.comments[i + 1].pos === skipWhiteSpace(file.text, comment.end)) {
if (rangeStartSingleLines === -1) {
rangeStartSingleLines = comment.pos;
}
}
else if (rangeStartSingleLines !== -1) {
addRange(rangeStartSingleLines, comment.end);
rangeStartSingleLines = -1;
}
else {
addRange(comment.pos, comment.end);
}
}
visitChildren(ast, addRangesForNode);
function addRangesForNode(node) {
if (node.kind === SyntaxKind.Doc) {
return; // fold doc comments as regular comments
}
let nodeStart = node.pos;
if ("decorators" in node && node.decorators.length > 0) {
const decoratorEnd = node.decorators[node.decorators.length - 1].end;
addRange(nodeStart, decoratorEnd);
nodeStart = skipTrivia(file.text, decoratorEnd);
}
addRange(nodeStart, node.end);
visitChildren(node, addRangesForNode);
}
return ranges;
function addRange(startPos, endPos) {
const start = file.getLineAndCharacterOfPosition(startPos);
const end = file.getLineAndCharacterOfPosition(endPos);
if (start.line !== end.line) {
ranges.push({
startLine: start.line,
startCharacter: start.character,
endLine: end.line,
endCharacter: end.character,
});
}
}
}
async function getDocumentSymbols(params) {
if (isTspConfigFile(params.textDocument))
return [];
const ast = await compileService.getScript(params.textDocument);
if (!ast) {
return [];
}
return getSymbolStructure(ast);
}
async function findDocumentHighlight(params) {
if (isTspConfigFile(params.textDocument))
return [];
const result = await compileInCoreMode(params.textDocument);
if (result === undefined) {
return [];
}
const { program, document, script } = result;
if (!document || !script) {
return [];
}
const identifiers = findReferenceIdentifiers(program, script, document.offsetAt(params.position), [script]);
return identifiers.map((identifier) => ({
range: getRange(identifier, script.file),
kind: DocumentHighlightKind.Read,
}));
}
function checkChange(change) {
const initVersion = fileService.getOpenDocumentInitVersion(change.document.uri);
if (!initVersion) {
// not expected, log something for troubleshooting
log({
level: "debug",
message: "Document change received for document not in documentsOpened set",
detail: change.document.uri,
});
documentOpened(change);
}
else if (initVersion < change.document.version) {
compileService.notifyChange(change.document, "changed");
}
else {
// do nothing for initVersion === change.document.version which should have been handled by didOpen event
}
}
function documentOpened(change) {
fileService.notifyDocumentOpened(change);
compileService.notifyChange(change.document, "opened");
}
async function reportDiagnostics({ program, document, optionsFromConfig }) {
if (!document)
return undefined;
if (isTspConfigFile(document))
return undefined;
const newDiagnosticIndex = new Map();
// Group diagnostics by file.
//
// Initialize diagnostics for all source files in program to empty array
// 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 = new Map();
diagnosticMap.set(document, []);
for (const each of program.sourceFiles.values()) {
const saved = each.file?.document;
if (saved) {
diagnosticMap.set(saved, []);
}
else {
// The file may be opened later when the compile used as cache
// so double check the fileService for opened document. Since
// TextDocuments will always return the same TextDocument instance
// so we should be good to use the TextDocument as the key here.
const opened = fileService.getOpenDocument(each.file.path);
if (opened) {
diagnosticMap.set(opened, []);
}
}
}
for (const each of program.diagnostics) {
const results = convertDiagnosticToLsp(fileService, program, document, each);
for (const result of results) {
const [diagnostic, diagDocument] = result;
if (each.url) {
diagnostic.codeDescription = {
href: each.url,
};
}
const unusedUsingRule = `${builtInLinterLibraryName}/${builtInLinterRule_UnusedUsing}`;
const unusedTemlateParameterRule = `${builtInLinterLibraryName}/${builtInLinterRule_UnusedTemplateParameter}`;
if (each.code === "deprecated") {
diagnostic.tags = [DiagnosticTag.Deprecated];
}
else if (each.code === unusedUsingRule) {
// Unused or unnecessary code. Diagnostics with this tag are rendered faded out, so no extra work needed from IDE side
// https://vscode-api.js.org/enums/vscode.DiagnosticTag.html#google_vignette
// https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.languageserver.protocol.diagnostictag?view=visualstudiosdk-2022
diagnostic.tags = [DiagnosticTag.Unnecessary];
if (optionsFromConfig.linterRuleSet?.enable?.[unusedUsingRule] === undefined &&
optionsFromConfig.linterRuleSet?.disable?.[unusedUsingRule] === undefined) {
// if the unused using is not configured by user explicitly, report it as hint by default
diagnostic.severity = DiagnosticSeverity.Hint;
}
}
else if (each.code === unusedTemlateParameterRule) {
// Unused or unnecessary code. Diagnostics with this tag are rendered faded out, so no extra work needed from IDE side
// https://vscode-api.js.org/enums/vscode.DiagnosticTag.html#google_vignette
// https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.languageserver.protocol.diagnostictag?view=visualstudiosdk-2022
diagnostic.tags = [DiagnosticTag.Unnecessary];
if (optionsFromConfig.linterRuleSet?.enable?.[unusedTemlateParameterRule] === undefined &&
optionsFromConfig.linterRuleSet?.disable?.[unusedTemlateParameterRule] === undefined) {
// if the unused template parameter is not configured by user explicitly, report it as hint by default
diagnostic.severity = DiagnosticSeverity.Hint;
}
}
diagnostic.data = {
id: diagnosticIdCounter++,
file: diagDocument.uri,
};
const diagnostics = diagnosticMap.get(diagDocument);
compilerAssert(diagnostics, "Diagnostic reported against a source file that was not added to the program.");
diagnostics.push(diagnostic);
newDiagnosticIndex.set(diagnostic.data.id, each);
}
}
// Report unused suppressions as hints with faded-out styling
for (const { directive } of program.suppressionTracker?.getUnusedSuppressions() ?? []) {
const unusedSuppressionDiagnostic = {
code: "unused-suppression",
severity: "warning",
message: `Suppression for "${directive.code}" is unused.`,
target: directive.node,
codefixes: [createRemoveUnusedSuppressionCodeFix(directive.node)],
};
const results = convertDiagnosticToLsp(fileService, program, document, unusedSuppressionDiagnostic);
for (const [diagnostic, diagDocument] of results) {
diagnostic.tags = [DiagnosticTag.Unnecessary];
diagnostic.severity = DiagnosticSeverity.Hint;
diagnostic.data = {
id: diagnosticIdCounter++,
file: diagDocument.uri,
};
const diagnostics = diagnosticMap.get(diagDocument);
compilerAssert(diagnostics, "Diagnostic reported against a source file that was not added to the program.");
diagnostics.push(diagnostic);
newDiagnosticIndex.set(diagnostic.data.id, unusedSuppressionDiagnostic);
}
}
// Atomically swap the diagnostic index so that in-flight code action resolves
// referencing old diagnostic IDs can still find their entries until new diagnostics are sent.
currentDiagnosticIndex = newDiagnosticIndex;
for (const [document, diagnostics] of diagnosticMap) {
sendDiagnostics(document, diagnostics);
}
}
async function getHover(params) {
if (isTspConfigFile(params.textDocument))
return { contents: [] };
const result = await compileInCoreMode(params.textDocument);
if (result === undefined) {
return { contents: [] };
}
const { program, document, script } = result;
if (!document || !script) {
return { contents: [] };
}
const id = getNodeAtPosition(script, document.offsetAt(params.position));
const sym = id?.kind === SyntaxKind.Identifier ? program.checker.resolveRelatedSymbols(id) : undefined;
if (!sym || sym.length === 0) {
return { contents: { kind: MarkupKind.Markdown, value: "" } };
}
else {
// Only show full definition if the symbol is a model or interface that has extends or is clauses.
// Avoid showing full definition in other cases which can be long and not useful
let includeExpandedDefinition = false;
const sn = getSymNode(sym[0]);
if (sn && sn.kind !== SyntaxKind.AliasStatement) {
const type = sym[0].type ?? program.checker.getTypeOrValueForNode(sn);
if (type && "kind" in type) {
const modelHasExtendOrIs = type.kind === "Model" &&
(type.baseModel !== undefined ||
type.sourceModel !== undefined ||
type.sourceModels.length > 0);
const interfaceHasExtend = type.kind === "Interface" && type.sourceInterfaces.length > 0;
includeExpandedDefinition = modelHasExtendOrIs || interfaceHasExtend;
}
}
const markdown = {
kind: MarkupKind.Markdown,
value: sym && sym.length > 0
? await getSymbolDetails(program, sym[0], {
includeSignature: true,
includeParameterTags: true,
includeExpandedDefinition,
})
: "",
};
return {
contents: markdown,
};
}
}
async function getSignatureHelp(params) {
if (isTspConfigFile(params.textDocument))
return undefined;
const result = await signatureHelpUpdateManager.scheduleUpdate(params.textDocument, "changed");
log({
level: "debug",
message: `getSignatureHelp got compile result: isUndefined = ${result === undefined}`,
});
if (result === undefined) {
return undefined;
}
const { script, document, program } = result;
if (!document || !script) {
return undefined;
}
const data = getSignatureHelpNodeAtPosition(script, document.offsetAt(params.position));
if (data === undefined) {
return undefined;
}
const { node, argumentIndex } = data;
switch (node.kind) {
case SyntaxKind.TypeReference:
return getSignatureHelpForTemplate(program, node, argumentIndex);
case SyntaxKind.DecoratorExpression:
case SyntaxKind.AugmentDecoratorStatement:
return getSignatureHelpForDecorator(program, node, argumentIndex);
default:
const _assertNever = node;
compilerAssert(false, "Unreachable");
}
}
async function getSignatureHelpForTemplate(program, node, argumentIndex) {
const sym = program.checker.resolveRelatedSymbols(node.target.kind === SyntaxKind.MemberExpression ? node.target.id : node.target);
if (!sym || sym.length <= 0) {
return undefined;
}
const templateDeclNode = sym[0].declarations[0];
if (!templateDeclNode ||
!("templateParameters" in templateDeclNode) ||
templateDeclNode.templateParameters.length === 0) {
return undefined;
}
const parameterDocs = getTemplateParameterDocumentation(templateDeclNode);
const parameters = templateDeclNode.templateParameters.map((x) => {
const info = { label: x.id.sv };
const doc = parameterDocs.get(x.id.sv);
if (doc) {
info.documentation = { kind: MarkupKind.Markdown, value: doc };
}
return info;
});
const help = {
signatures: [
{
label: `${sym[0].name}<${parameters.map((x) => x.label).join(", ")}>`,
parameters,
activeParameter: Math.min(parameters.length - 1, argumentIndex),
},
],
activeSignature: 0,
activeParameter: 0,
};
const doc = await getSymbolDetails(program, sym[0], {
includeSignature: false,
includeParameterTags: false,
});
if (doc) {
help.signatures[0].documentation = { kind: MarkupKind.Markdown, value: doc };
}
return help;
}
async function getSignatureHelpForDecorator(program, node, argumentIndex) {
const sym = program.checker.resolveRelatedSymbols(node.target.kind === SyntaxKind.MemberExpression ? node.target.id : node.target);
if (!sym || sym.length <= 0) {
return undefined;
}
const decoratorDeclNode = sym[0].declarations.find((x) => x.kind === SyntaxKind.DecoratorDeclarationStatement);
if (decoratorDeclNode === undefined) {
return undefined;
}
const type = program.checker.getTypeForNode(decoratorDeclNode);
compilerAssert(type.kind === "Decorator", "Expected type to be a decorator.");
const parameterDocs = getParameterDocumentation(program, type);
let labelPrefix = "";
const parameters = [];
if (node.kind === SyntaxKind.AugmentDecoratorStatement) {
const targetType = decoratorDeclNode.target.type
? program.checker.getTypeForNode(decoratorDeclNode.target.type)
: undefined;
parameters.push({
label: `${decoratorDeclNode.target.id.sv}: ${targetType ? getTypeName(targetType) : "unknown"}`,
});
labelPrefix = "@";
}
parameters.push(...type.parameters.map((x) => {
const info = {
// prettier-ignore
label: `${x.rest ? "..." : ""}${x.name}${x.optional ? "?" : ""}: ${getEntityName(x.type)}`,
};
const doc = parameterDocs.get(x.name);
if (doc) {
info.documentation = { kind: MarkupKind.Markdown, value: doc };
}
return info;
}));
const help = {
signatures: [
{
label: `${labelPrefix}${type.name}(${parameters.map((x) => x.label).join(", ")})`,
parameters,
activeParameter: Math.min(parameters.length - 1, argumentIndex),
},
],
activeSignature: 0,
activeParameter: 0,
};
const doc = await getSymbolDetails(program, sym[0], {
includeSignature: false,
includeParameterTags: false,
});
if (doc) {
help.signatures[0].documentation = { kind: MarkupKind.Markdown, value: doc };
}
return help;
}
async function formatDocument(params) {
if (isTspConfigFile(params.textDocument))
return [];
const document = host.getOpenDocumentByURL(params.textDocument.uri);
if (document === undefined) {
return [];
}
const path = await fileService.fileURLToRealPath(params.textDocument.uri);
const prettierConfig = await resolvePrettierConfig(path);
const resolvedConfig = prettierConfig ?? {
tabWidth: params.options.tabSize,
useTabs: !params.options.insertSpaces,
};
log({
level: "info",
message: `Formatting TypeSpec document: ${JSON.stringify({ fileUri: params.textDocument.uri, vscodeOptions: params.options, prettierConfig, resolvedConfig }, null, 2)}`,
});
try {
const formattedText = await formatTypeSpec(document.getText(), resolvedConfig);
return [minimalEdit(document, formattedText)];
}
catch (error) {
log({
level: "error",
message: `Failed to format TypeSpec document: ${params.textDocument.uri}`,
detail: error,
});
return [];
}
}
async function resolvePrettierConfig(path) {
try {
// Resolve prettier if it is installed.
const prettier = await import("prettier");
return prettier.resolveConfig(path);
}
catch (e) {
return null;
}
}
function minimalEdit(document, string1) {
const string0 = document.getText();
// length of common prefix
let i = 0;
while (i < string0.length && i < string1.length && string0[i] === string1[i]) {
++i;
}
// length of common suffix
let j = 0;
while (i + j < string0.length &&
i + j < string1.length &&
string0[string0.length - j - 1] === string1[string1.length - j - 1]) {
++j;
}
const newText = string1.substring(i, string1.length - j);
const pos0 = document.positionAt(i);
const pos1 = document.positionAt(string0.length - j);
return TextEdit.replace(Range.create(pos0, pos1), newText);
}
async function gotoDefinition(params) {
if (isTspConfigFile(params.textDocument))
return [];
const result = await compileInCoreMode(params.textDocument);
if (result === undefined || result.document === undefined || result.script === undefined) {
return [];
}
const node = getNodeAtPosition(result.script, result.document.offsetAt(params.position));
switch (node?.kind) {
case SyntaxKind.Identifier:
const sym = result.program.checker.resolveRelatedSymbols(node);
return getLocations(sym && sym.length > 0 ? sym[0].declarations : undefined);
case SyntaxKind.StringLiteral:
if (node.parent?.kind === SyntaxKind.ImportStatement) {
return [await getImportLocation(node.value, result.script)];
}
else {
return [];
}
}
return [];
}
async function getImportLocation(importPath, currentFile) {
const host = {
realpath: compilerHost.realpath,
readFile: async (path) => {
const file = await compilerHost.readFile(path);
return file.text;
},
stat: compilerHost.stat,
};
const resolved = await resolveModule(host, importPath, {
baseDir: getDirectoryPath(currentFile.file.path),
directoryIndexFiles: ["main.tsp", "index.mjs", "index.js"],
resolveMain(pkg) {
// this lets us follow node resolve semantics more-or-less exactly
// but using tspMain instead of main.
return resolveTspMain(pkg) ?? pkg.main;
},
conditions: ["typespec"],
fallbackOnMissingCondition: true,
});
return {
uri: fileService.getURL(resolved.type === "file" ? resolved.path : resolved.mainFile),
range: Range.create(0, 0, 0, 0),
};
}
async function complete(params) {
if (isTspConfigFile(params.textDocument)) {
const doc = host.getOpenDocumentByURL(params.textDocument.uri);
if (doc) {
const items = await provideTspconfigCompletionItems(doc, params.position, {
fileService,
compilerHost,
emitterProvider,
linterProvider,
log,
});
return CompletionList.create(items);
}
return CompletionList.create([]);
}
const completions = {
isIncomplete: false,
items: [],
};
const result = await compileInCoreMode(params.textDocument);
if (result) {
const { script, document, program } = result;
if (!document || !script) {
return completions;
}
const posDetail = getCompletionNodeAtPosition(script, document.offsetAt(params.position));
return await resolveCompletion({
program,
file: script,
completions,
params,
}, posDetail);
}
return completions;
}
async function findReferences(params) {
if (isTspConfigFile(params.textDocument))
return [];
const result = await compileInCoreMode(params.textDocument);
if (result === undefined || result.document === undefined || result.script === undefined) {
return [];
}
const identifiers = findReferenceIdentifiers(result.program, result.script, result.document.offsetAt(params.position));
return getLocations(identifiers);
}
async function prepareRename(params) {
if (isTspConfigFile(params.textDocument))
return undefined;
const result = await compileInCoreMode(params.textDocument);
if (result === undefined || result.document === undefined || result.script === undefined) {
return undefined;
}
const id = getNodeAtPosition(result.script, result.document.offsetAt(params.position));
return id?.kind === SyntaxKind.Identifier ? getLocation(id)?.range : undefined;
}
async function rename(params) {
if (isTspConfigFile(params.textDocument))
return { changes: {} };
const changes = {};
const result = await compileInCoreMode(params.textDocument);
if (result && result.document && result.script) {
const identifiers = findReferenceIdentifiers(result.program, result.script, result.document.offsetAt(params.position));
for (const id of identifiers) {
const location = getLocation(id);
if (!location) {
continue;
}
const change = TextEdit.replace(location.range, params.newName);
if (location.uri in changes) {
changes[location.uri].push(change);
}
else {
changes[location.uri] = [change];
}
}
}
return { changes };
}
function findReferenceIdentifiers(program, file, pos, searchFiles = program.sourceFiles.values()) {
const id = getNodeAtPosition(file, pos);
if (id?.kind !== SyntaxKind.Identifier) {
return [];
}
const sym = program.checker.resolveRelatedSymbols(id);
if (!sym || sym.length <= 0) {
return [id];
}
const references = [];
for (const searchFile of searchFiles) {
visitChildren(searchFile, function visit(node) {
if (node.kind === SyntaxKind.Identifier) {
const s = program.checker.resolveRelatedSymbols(node);
if (!s || s.length <= 0) {
return;
}
if (s.some((candidate) => sym.some((target) => candidate === target || (target.type && candidate.type === target.type)))) {
references.push(node);
}
}
visitChildren(node, visit);
});
}
return references;
}
async function getSemanticTokensForDocument(params) {
if (isTspConfigFile(params.textDocument))
return [];
const ast = await compileService.getScri