svelte-language-server
Version:
A language server for Svelte
891 lines • 41.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SvelteCheckTSGoDiagnosticsProvider = void 0;
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = require("node:path");
const path_1 = __importDefault(require("path"));
const svelte2tsx_1 = require("svelte2tsx");
const typescript_1 = __importDefault(require("typescript"));
const vscode_languageserver_1 = require("vscode-languageserver");
const importPackage_1 = require("../../../importPackage");
const documents_1 = require("../../../lib/documents");
const configLoader_1 = require("../../../lib/documents/configLoader");
const fileCollection_1 = require("../../../lib/documents/fileCollection");
const logger_1 = require("../../../logger");
const utils_1 = require("../../../utils");
const DocumentSnapshot_1 = require("../../typescript/DocumentSnapshot");
const DiagnosticsProvider_1 = require("../../typescript/features/DiagnosticsProvider");
const utils_2 = require("../../typescript/features/utils");
const svelte_ast_utils_1 = require("../../typescript/svelte-ast-utils");
const utils_3 = require("../../typescript/utils");
const utils_4 = require("./utils");
const VIRTUAL_SUFFIX = '_virtual__';
const SVELTE_EXT_LENGTH = '.svelte'.length;
const D_SVELTE_TS_EXTENSION = '.d.svelte.ts';
const D_SVELTE_TS_LENGTH = D_SVELTE_TS_EXTENSION.length;
const SVELTE_DTS_EXTENSION = '.svelte.d.ts';
class SvelteCheckTSGoDiagnosticsProvider {
constructor(apiModule, tsAstModule, tsconfigPath, ambientTypesSource, createDocument) {
this.files = new fileCollection_1.FileMap();
this.virtualFiles = new fileCollection_1.FileMap();
this.pendingChanges = {
created: new Set(),
changed: new Set(),
deleted: new Set()
};
this.tsApiModule = apiModule;
this.tsAstModule = tsAstModule;
this.tsconfigPath = tsconfigPath;
this.virtualTsconfigPath = path_1.default.join(path_1.default.dirname(tsconfigPath), `tsconfig${VIRTUAL_SUFFIX}.json`);
this.createDocument = createDocument;
this.ambientTypesSource = ambientTypesSource;
const sveltePackage = (0, importPackage_1.importSvelte)(tsconfigPath);
this.snapshotOptions = {
parse: sveltePackage.parse,
transformOnTemplateError: false,
typingsNamespace: 'svelteHTML',
version: sveltePackage.VERSION,
emitJsDoc: true
};
this.api = new apiModule.API({
fs: this.createFsProxy()
});
this.projectConfig = this.parseConfig();
if (this.projectConfig?.raw.svelteOptions?.namespace) {
this.snapshotOptions.typingsNamespace = this.projectConfig.raw.svelteOptions.namespace;
}
this.pendingConfigLoading = configLoader_1.configLoader.loadConfigs(path_1.default.dirname(tsconfigPath));
this.writeVirtualTsconfig();
}
async getDiagnostics(document, cancellationToken) {
const filePath = document.getFilePath();
if (!filePath) {
return [];
}
const project = await this.getProject();
if (!project) {
return [];
}
const tsDoc = this.files.get((0, utils_1.normalizePath)(filePath));
if (!tsDoc || !(tsDoc instanceof DocumentSnapshot_1.SvelteDocumentSnapshot)) {
return [];
}
if (['coffee', 'coffeescript'].includes(document.getLanguageAttribute('script')) ||
cancellationToken?.isCancellationRequested) {
return [];
}
// Document preprocessing failed, show parser error instead
const parserErrorDiag = getParserErrorDiagnostic(tsDoc);
if (parserErrorDiag) {
return [parserErrorDiag];
}
const virtualPath = toVirtualPath(tsDoc);
const diagnostics = [];
const program = project.program;
diagnostics.push(...(program.getSyntacticDiagnostics(virtualPath) || []));
diagnostics.push(...(program.getSuggestionDiagnostics(virtualPath) || []));
diagnostics.push(...(program.getSemanticDiagnostics(virtualPath) || []));
return mapAndFilterDiagnostics(this.tsAstModule, this.tsApiModule, project, diagnostics, document, tsDoc);
}
async getProject() {
if (this.pendingConfigLoading) {
await this.pendingConfigLoading;
this.pendingConfigLoading = undefined;
}
const snapshot = this.api.updateSnapshot({
openProject: this.virtualTsconfigPath,
fileChanges: {
created: Array.from(this.pendingChanges.created),
changed: Array.from(this.pendingChanges.changed),
deleted: Array.from(this.pendingChanges.deleted)
}
});
this.pendingChanges.created.clear();
this.pendingChanges.changed.clear();
this.pendingChanges.deleted.clear();
const project = snapshot.getProject(this.virtualTsconfigPath);
return project;
}
getAllSvelteFiles() {
return Array.from(this.files.keys()).filter(utils_3.isSvelteFilePath);
}
mapAndFilterDiagnostics(project, diagnostics) {
const byFile = new Map();
for (const diag of diagnostics) {
const key = toRealPath(diag.fileName ?? this.tsconfigPath);
let bucket = byFile.get(key);
if (!bucket) {
bucket = [];
byFile.set(key, bucket);
}
bucket.push(diag);
}
const result = [];
for (const [fileName, diags] of byFile) {
const tsDoc = this.files.get((0, utils_1.normalizePath)(fileName));
if (!tsDoc) {
result.push(this.covertDiagnosticsForUnopenedFile(fileName, diags));
continue;
}
if (tsDoc instanceof DocumentSnapshot_1.SvelteDocumentSnapshot) {
const mappedDiags = mapAndFilterDiagnostics(this.tsAstModule, this.tsApiModule, project, diags, tsDoc.parent, tsDoc);
result.push({
filePath: fileName,
text: tsDoc.parent.getText(),
diagnostics: mappedDiags
});
}
else if (tsDoc instanceof DocumentSnapshot_1.JSOrTSDocumentSnapshot) {
const mappedDiags = diags.map((diag) => {
return {
range: {
start: tsDoc.getOriginalPosition(tsDoc.positionAt(diag.pos)),
end: tsDoc.getOriginalPosition(tsDoc.positionAt(diag.end))
},
message: flattenDiagnosticMessage(diag),
severity: (0, utils_3.mapSeverity)(diag.category),
source: diag.fileName?.endsWith('js') ? 'js' : 'ts',
tags: getDiagnosticTag(diag),
code: diag.code
};
});
result.push({
filePath: fileName,
text: tsDoc.originalText,
diagnostics: mappedDiags
});
}
else {
result.push(this.covertDiagnosticsForUnopenedFile(fileName, diags));
}
}
return result;
}
async getDiagnosticsForPullMode(document) {
return {
items: await this.getDiagnostics(document),
kind: 'full'
};
}
watchUpdate(doc, kind) {
const filePath = (0, utils_1.urlToPath)(doc.uri) || '';
const normalizedPath = (0, utils_1.normalizePath)(filePath);
if (kind === 'created') {
if ((0, utils_3.isSvelteFilePath)(normalizedPath)) {
// trigger project files invalidation should be enough
this.pendingChanges.created.add(changeExtension(normalizedPath, D_SVELTE_TS_EXTENSION));
}
else {
this.pendingChanges.created.add(normalizedPath);
}
}
else if (kind === 'deleted') {
this.pendingChanges.deleted.add(normalizedPath);
}
else if (kind === 'changed') {
let changedPath = normalizedPath;
if (this.files.has(normalizedPath)) {
const newSnapshot = DocumentSnapshot_1.DocumentSnapshot.fromFilePath(filePath, this.createDocument, this.snapshotOptions, typescript_1.default.sys);
if (newSnapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot) {
changedPath = toVirtualPath(newSnapshot);
}
this.virtualFiles.set(changedPath, newSnapshot.getFullText());
this.files.set(normalizedPath, newSnapshot);
}
this.pendingChanges.changed.add(changedPath);
}
}
getProjectConfig() {
return this.projectConfig;
}
createFsProxy() {
const service = this;
const kitFiles = {
serverHooksPath: 'src/hooks.client',
clientHooksPath: 'src/hooks.client',
universalHooksPath: 'src/hooks',
paramsPath: 'src/params'
};
return {
getAccessibleEntries(directory) {
const realFiles = [];
const directories = [];
const resultFiles = [];
try {
const entries = node_fs_1.default.readdirSync(directory, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile()) {
realFiles.push(entry.name);
}
else if (entry.isDirectory()) {
directories.push(entry.name);
}
else if (entry.isSymbolicLink()) {
const fullPath = path_1.default.join(directory, entry.name);
const stats = node_fs_1.default.statSync(node_fs_1.default.realpathSync(fullPath));
if (stats.isFile()) {
realFiles.push(entry.name);
}
else if (stats.isDirectory()) {
directories.push(entry.name);
}
}
}
for (const file of realFiles) {
addFileEntry(path_1.default.join(directory, file), realFiles, resultFiles);
}
return {
files: resultFiles,
directories
};
}
catch (error) {
logger_1.Logger.error(`Error reading directory ${directory}:`, error);
return undefined;
}
},
readFile(path) {
const normalizedPath = (0, utils_1.normalizePath)(path);
const virtualEntry = service.virtualFiles.get(normalizedPath);
if (virtualEntry !== undefined) {
return virtualEntry;
}
if (normalizedPath.endsWith('node_modules/svelte/types/ambient.d.ts')) {
return '';
}
if (normalizedPath.endsWith('node_modules/svelte/types/index.d.ts') ||
svelte2tsx_1.internalHelpers.isKitFile(normalizedPath, kitFiles)) {
const snapshot = DocumentSnapshot_1.DocumentSnapshot.fromFilePath(normalizedPath, service.createDocument, service.snapshotOptions, typescript_1.default.sys);
service.files.set(normalizedPath, snapshot);
return snapshot.getFullText();
}
// undefined to signal api to read from disk by itself
return undefined;
},
fileExists(filePath) {
const virtualFiles = service.virtualFiles;
if (filePath.includes(VIRTUAL_SUFFIX) &&
virtualFiles.has((0, utils_1.normalizePath)(filePath))) {
return true;
}
if (filePath.endsWith(D_SVELTE_TS_EXTENSION)) {
if (virtualFiles.has((0, utils_1.normalizePath)(filePath)) || node_fs_1.default.existsSync(filePath)) {
return true;
}
const withoutExtension = filePath.slice(0, -D_SVELTE_TS_LENGTH);
const svelteDtsPath = withoutExtension + SVELTE_DTS_EXTENSION;
if (node_fs_1.default.existsSync(svelteDtsPath)) {
service.addDtsRedirect(filePath, path_1.default.basename(svelteDtsPath));
return true;
}
const targetSvelteFile = withoutExtension + '.svelte';
if (node_fs_1.default.existsSync(targetSvelteFile)) {
const virtualBaseName = service.addVirtualSvelteFile(targetSvelteFile);
service.addDtsRedirect(filePath, virtualBaseName);
return true;
}
}
// undefined to signal api to check existence by itself
return undefined;
}
};
function addFileEntry(fullPath, realFiles, resultFiles) {
const name = path_1.default.basename(fullPath);
if (!fullPath.endsWith('.svelte')) {
resultFiles.push(name);
return;
}
const virtualBaseName = service.addVirtualSvelteFile(fullPath);
resultFiles.push(virtualBaseName);
const dSvelteTsPath = changeExtension(fullPath, D_SVELTE_TS_EXTENSION);
const dSvelteTsName = path_1.default.basename(dSvelteTsPath);
if (realFiles.includes(dSvelteTsName)) {
return;
}
const svelteDtsName = changeExtension(name, SVELTE_DTS_EXTENSION);
const dSvelteTsTarget = realFiles.includes(svelteDtsName)
? svelteDtsName
: virtualBaseName;
service.addDtsRedirect(dSvelteTsPath, dSvelteTsTarget);
resultFiles.push(dSvelteTsName);
}
}
parseConfig() {
const commandLine = typescript_1.default.parseJsonSourceFileConfigFileContent(typescript_1.default.parseJsonText(this.tsconfigPath, typescript_1.default.sys.readFile(this.tsconfigPath) || ''), {
...typescript_1.default.sys,
readDirectory() {
// skip project file searching
return [];
}
}, path_1.default.dirname(this.tsconfigPath));
return commandLine;
}
writeVirtualTsconfig() {
const commandLine = this.projectConfig;
const tsconfigPath = this.tsconfigPath;
const sveltePackageInfo = (0, importPackage_1.getPackageInfo)('svelte', this.virtualTsconfigPath);
let svelteTsPath;
try {
// For when svelte2tsx/svelte-check is part of node_modules, for example VS Code extension
svelteTsPath = (0, node_path_1.dirname)(require.resolve(this.ambientTypesSource));
}
catch (e) {
// Fall back to dirname
svelteTsPath = __dirname;
}
const svelteTsxFiles = svelte2tsx_1.internalHelpers.get_global_types(typescript_1.default.sys, sveltePackageInfo.version.major === 3, sveltePackageInfo.path, svelteTsPath, undefined);
const filesConfig = commandLine.raw.files;
const rebasedFiles = [];
if (filesConfig) {
for (const file of filesConfig) {
if (file.endsWith('.svelte')) {
const virtualBaseName = this.addVirtualSvelteFile(path_1.default.join(path_1.default.dirname(tsconfigPath), file));
rebasedFiles.push(virtualBaseName);
// let fileExists handle the .d.svelte.ts redirection
}
else {
rebasedFiles.push(file);
}
}
}
const virtualTsConfigContent = JSON.stringify({
extends: './' + path_1.default.basename(tsconfigPath),
compilerOptions: { allowArbitraryExtensions: true },
files: rebasedFiles.concat(svelteTsxFiles),
// otherwise only "files" will be included and not the default "everything"
include: commandLine.raw.include ? undefined : ['**/*']
});
this.virtualFiles.set((0, utils_1.normalizePath)(this.virtualTsconfigPath), virtualTsConfigContent);
}
addDtsRedirect(dSvelteTsPath, targetFileName) {
const specifierFileName = targetFileName.endsWith(SVELTE_DTS_EXTENSION)
? targetFileName.replace(SVELTE_DTS_EXTENSION, '.svelte.js')
: changeExtension(targetFileName, '.js');
const dtsImportPath = './' + specifierFileName;
const dtsContent = `export { default } from "${dtsImportPath}";\nexport * from "${dtsImportPath}";\n`;
this.virtualFiles.set((0, utils_1.normalizePath)(dSvelteTsPath), dtsContent);
}
addVirtualSvelteFile(filePath) {
const svelteFile = DocumentSnapshot_1.DocumentSnapshot.fromFilePath(filePath, this.createDocument, this.snapshotOptions, typescript_1.default.sys);
const normalizedPath = (0, utils_1.normalizePath)(filePath);
this.files.set(normalizedPath, svelteFile);
const virtualPath = toVirtualPath(svelteFile);
this.virtualFiles.set(virtualPath, svelteFile.getFullText());
const virtualBasename = path_1.default.basename(virtualPath);
return virtualBasename;
}
covertDiagnosticsForUnopenedFile(filePath, diagnostics) {
const text = typescript_1.default.sys.readFile(filePath) ?? '';
const result = [];
const lineOffsets = (0, documents_1.getLineOffsets)(text);
for (const diag of diagnostics) {
result.push({
range: {
start: (0, documents_1.positionAt)(diag.pos, text, lineOffsets),
end: (0, documents_1.positionAt)(diag.end, text, lineOffsets)
},
severity: (0, utils_3.mapSeverity)(diag.category),
message: flattenDiagnosticMessage(diag),
code: diag.code,
source: diag.fileName?.endsWith('js') ? 'js' : 'ts',
tags: getDiagnosticTag(diag)
});
}
return { filePath, diagnostics: result, text };
}
dispose() {
this.api.close();
}
}
exports.SvelteCheckTSGoDiagnosticsProvider = SvelteCheckTSGoDiagnosticsProvider;
function changeExtension(path, newExtension) {
return path.slice(0, path.lastIndexOf('.')) + newExtension;
}
function getParserErrorDiagnostic(tsDoc) {
if (!tsDoc.parserError) {
return;
}
return {
range: tsDoc.parserError.range,
severity: vscode_languageserver_1.DiagnosticSeverity.Error,
source: tsDoc.scriptKind === typescript_1.default.ScriptKind.TSX || tsDoc.scriptKind === typescript_1.default.ScriptKind.TS
? 'ts'
: 'js',
message: tsDoc.parserError.message,
code: tsDoc.parserError.code
};
}
function toVirtualPath(snapshot) {
const ext = snapshot.scriptKind === typescript_1.default.ScriptKind.TS ? '.ts' : '.js';
return (0, utils_1.normalizePath)(snapshot.filePath.slice(0, -SVELTE_EXT_LENGTH) + VIRTUAL_SUFFIX + ext);
}
function toRealPath(path) {
if (!path.includes(VIRTUAL_SUFFIX)) {
return path;
}
return path.slice(0, -3).replace(VIRTUAL_SUFFIX, '') + '.svelte';
}
function mapAndFilterDiagnostics(tsAstModule, tsApiModule, tsApiProject, diagnostics, document, tsDoc) {
// For svelte-check tsgo, we called api to get all diagnostics instead of calling getDiagnostics for each file separately. So we also need to check parser error or coffeescript files here.
if (['coffee', 'coffeescript'].includes(document.getLanguageAttribute('script'))) {
return [];
}
// Document preprocessing failed, show parser error instead
const parserErrorDiag = getParserErrorDiagnostic(tsDoc);
if (parserErrorDiag) {
return [parserErrorDiag];
}
const notGenerated = isNotGenerated(tsDoc.getFullText());
const additionalStoreDiagnostics = [];
for (const diagnostic of diagnostics) {
if ((diagnostic.code === DiagnosticsProvider_1.DiagnosticCode.NO_OVERLOAD_MATCHES_CALL ||
diagnostic.code === DiagnosticsProvider_1.DiagnosticCode.ARG_TYPE_X_NOT_ASSIGNABLE_TO_TYPE_Y) &&
!notGenerated(diagnostic) &&
diagnostic.fileName) {
if ((0, utils_2.isStoreVariableIn$storeDeclaration)(tsDoc.getFullText(), diagnostic.pos)) {
const storeName = tsDoc.getFullText().substring(diagnostic.pos, diagnostic.end);
const symbol = tsApiProject.checker.getSymbolAtPosition(diagnostic.fileName, (0, utils_2.get$storeOffsetOf$storeDeclaration)(tsDoc.getFullText(), diagnostic.pos));
if (!symbol) {
continue;
}
const sourceFile = tsApiProject.program.getSourceFile(diagnostic.fileName);
if (!sourceFile) {
continue;
}
const storeUsages = tsApiProject.checker.getReferencesToSymbolInFile(diagnostic.fileName, symbol);
for (const storeUsage of storeUsages) {
const node = storeUsage.resolve(tsApiProject);
if (node) {
additionalStoreDiagnostics.push({
...diagnostic,
text: `Cannot use '${storeName}' as a store. '${storeName}' needs to be an object with a subscribe method on it.\n\n` +
diagnostic.text,
pos: (0, utils_4.getStartOfNode)(tsAstModule, node, sourceFile),
end: node.end
});
}
}
}
}
}
diagnostics.push(...additionalStoreDiagnostics);
diagnostics = diagnostics
.filter(notGenerated)
.filter((diagnostic) => !isUnusedReactiveStatementLabel(tsAstModule, tsApiProject, diagnostic))
.filter((diagnostic) => !expectedTransitionThirdArgument(tsAstModule, diagnostic, tsDoc, tsApiProject));
diagnostics = resolveNoopsInReactiveStatements(tsAstModule, tsApiProject, diagnostics);
const source = tsDoc.scriptKind === typescript_1.default.ScriptKind.TSX || tsDoc.scriptKind === typescript_1.default.ScriptKind.TS
? 'ts'
: 'js';
const mapRange = rangeMapper(tsAstModule, tsApiModule, tsApiProject, tsDoc, document);
const noFalsePositive = isNoFalsePositive(document, tsDoc);
const converted = [];
for (const tsDiag of diagnostics) {
let diagnostic = {
range: { start: tsDoc.positionAt(tsDiag.pos), end: tsDoc.positionAt(tsDiag.end) },
severity: (0, utils_3.mapSeverity)(tsDiag.category),
source,
message: flattenDiagnosticMessage(tsDiag),
code: tsDiag.code,
tags: getDiagnosticTag(tsDiag)
};
diagnostic = mapRange(diagnostic);
moveBindingErrorMessage(tsDiag, tsDoc, diagnostic, document);
if (!hasNoNegativeLines(diagnostic) || !noFalsePositive(diagnostic)) {
continue;
}
diagnostic = adjustIfNecessary(diagnostic, tsDoc.isSvelte5Plus);
diagnostic = swapDiagRangeStartEndIfNecessary(diagnostic);
converted.push(diagnostic);
}
return converted;
}
function flattenDiagnosticMessage(diag, level = 0) {
if (!diag.messageChain) {
return diag.text;
}
let messages = [diag.text];
for (let i = 0; i < diag.messageChain.length; i++) {
const chainedDiag = diag.messageChain[i];
const indent = ' '.repeat(level + 1);
messages.push(indent + flattenDiagnosticMessage(chainedDiag, level + 1));
}
return messages.join('\n');
}
function moveBindingErrorMessage(tsDiag, tsDoc, diagnostic, document) {
if (tsDiag.code === DiagnosticsProvider_1.DiagnosticCode.TYPE_X_NOT_ASSIGNABLE_TO_TYPE_Y &&
tsDiag.pos &&
tsDoc.getText(tsDiag.pos, tsDiag.end).endsWith('.$$bindings')) {
let node = tsDoc.svelteNodeAt(diagnostic.range.start);
while (node && node.type !== 'InlineComponent') {
node = node.parent;
}
if (node) {
let name = tsDoc.getText(tsDiag.end, tsDiag.end + 100);
const quoteIdx = name.indexOf("'");
name = name.substring(quoteIdx + 1, name.indexOf("'", quoteIdx + 1));
const binding = node.attributes.find((attr) => attr.type === 'Binding' && attr.name === name);
if (binding) {
// try to make the error more readable for english users
if (diagnostic.message.startsWith("Type '") &&
diagnostic.message.includes("is not assignable to type '")) {
const idx = diagnostic.message.indexOf(`Type '"`) + `Type '"`.length;
const propName = diagnostic.message.substring(idx, diagnostic.message.indexOf('"', idx));
diagnostic.message =
"Cannot use 'bind:' with this property. It is declared as non-bindable inside the component.\n" +
`To mark a property as bindable: 'let { ${propName} = $bindable() } = $props()'`;
}
else {
diagnostic.message =
"Cannot use 'bind:' with this property. It is declared as non-bindable inside the component.\n" +
`To mark a property as bindable: 'let { prop = $bindable() } = $props()'\n\n` +
diagnostic.message;
}
diagnostic.range = {
start: document.positionAt(binding.start),
end: document.positionAt(binding.end)
};
}
}
}
}
function rangeMapper(tsAstModule, tsApiModule, project, snapshot, document) {
const get$$PropsDefWithCache = (0, utils_1.memoize)(() => get$$PropsDef(tsAstModule, project, snapshot));
const get$$PropsAliasInfoWithCache = (0, utils_1.memoize)(() => get$$PropsAliasForInfo(tsAstModule, tsApiModule, project, get$$PropsDefWithCache, document));
return (diagnostic) => {
let range = (0, documents_1.mapRangeToOriginal)(snapshot, diagnostic.range);
if (range.start.line < 0) {
range =
movePropsErrorRangeBackIfNecessary(tsAstModule, diagnostic, snapshot, get$$PropsDefWithCache, get$$PropsAliasInfoWithCache) ?? range;
}
if (([DiagnosticsProvider_1.DiagnosticCode.MISSING_PROP, DiagnosticsProvider_1.DiagnosticCode.MISSING_PROPS].includes(diagnostic.code) ||
(DiagnosticsProvider_1.DiagnosticCode.TYPE_X_NOT_ASSIGNABLE_TO_TYPE_Y &&
diagnostic.message.includes("'Properties<"))) &&
!(0, utils_3.hasNonZeroRange)({ range })) {
const node = (0, documents_1.getNodeIfIsInStartTag)(document.html, document.offsetAt(range.start));
if (node) {
// This is a "some prop missing" error on a component -> remap
range.start = document.positionAt(node.start + 1);
range.end = document.positionAt(node.start + 1 + (node.tag?.length || 1));
}
}
return { ...diagnostic, range };
};
}
function findDiagnosticNode(tsAstModule, diagnostic, sourceFile) {
const touchingNode = tsAstModule.getTouchingToken(sourceFile, diagnostic.pos);
if (touchingNode.end === diagnostic.end) {
return touchingNode;
}
let current = touchingNode.parent;
while (current.pos === touchingNode.pos) {
if (current.end === diagnostic.end) {
return current;
}
current = current.parent;
}
}
function copyDiagnosticAndChangeNode(tsAstModule, diagnostic, sourceFile) {
return (node) => {
const start = (0, utils_4.getStartOfNode)(tsAstModule, node, sourceFile);
return {
...diagnostic,
pos: start,
end: node.end
};
};
}
/**
* In some rare cases mapping of diagnostics does not work and produces negative lines.
* We filter out these diagnostics with negative lines because else the LSP
* apparently has a hickup and does not show any diagnostics at all.
*/
function hasNoNegativeLines(diagnostic) {
return diagnostic.range.start.line >= 0 && diagnostic.range.end.line >= 0;
}
const generatedVarRegex = /'\$\$_\w+(\.\$on)?'/;
function isNoFalsePositive(document, tsDoc) {
const text = document.getText();
const usesPug = document.getLanguageAttribute('template') === 'pug';
return (diagnostic) => {
if ([DiagnosticsProvider_1.DiagnosticCode.MULTIPLE_PROPS_SAME_NAME, DiagnosticsProvider_1.DiagnosticCode.DUPLICATE_IDENTIFIER].includes(diagnostic.code)) {
const node = tsDoc.svelteNodeAt(diagnostic.range.start);
if ((0, svelte_ast_utils_1.isAttributeName)(node, 'Element') || (0, svelte_ast_utils_1.isEventHandler)(node, 'Element')) {
return false;
}
}
if (diagnostic.code === DiagnosticsProvider_1.DiagnosticCode.DEPRECATED_SIGNATURE &&
generatedVarRegex.test(diagnostic.message)) {
// Svelte 5: $on and constructor is deprecated, but we don't want to show this warning for generated code
return false;
}
return (isNoUsedBeforeAssigned(diagnostic, text, tsDoc) &&
(!usesPug || isNoPugFalsePositive(diagnostic, document)));
};
}
/**
* All diagnostics inside the template tag and the unused import/variable diagnostics
* are marked as false positive.
*/
function isNoPugFalsePositive(diagnostic, document) {
return (!(0, documents_1.isRangeInTag)(diagnostic.range, document.templateInfo) &&
diagnostic.code !== DiagnosticsProvider_1.DiagnosticCode.NEVER_READ &&
diagnostic.code !== DiagnosticsProvider_1.DiagnosticCode.ALL_IMPORTS_UNUSED);
}
/**
* Variable used before being assigned, can happen when you do `export let x`
* without assigning a value in strict mode. Should not throw an error here
* but on the component-user-side ("you did not set a required prop").
*/
function isNoUsedBeforeAssigned(diagnostic, text, tsDoc) {
if (diagnostic.code !== DiagnosticsProvider_1.DiagnosticCode.USED_BEFORE_ASSIGNED) {
return true;
}
return !tsDoc.hasProp((0, documents_1.getTextInRange)(diagnostic.range, text));
}
/**
* Some diagnostics have JSX-specific or confusing nomenclature. Enhance/adjust them for more clarity.
*/
function adjustIfNecessary(diagnostic, isSvelte5Plus) {
if (diagnostic.code === DiagnosticsProvider_1.DiagnosticCode.ARG_TYPE_X_NOT_ASSIGNABLE_TO_TYPE_Y &&
diagnostic.message.includes('ConstructorOfATypedSvelteComponent')) {
return {
...diagnostic,
message: diagnostic.message +
'\n\nPossible causes:\n' +
'- You use the instance type of a component where you should use the constructor type\n' +
'- Type definitions are missing for this Svelte Component. ' +
(isSvelte5Plus
? ''
: 'If you are using Svelte 3.31+, use SvelteComponentTyped to add a definition:\n' +
' import type { SvelteComponentTyped } from "svelte";\n' +
' class ComponentName extends SvelteComponentTyped<{propertyName: string;}> {}')
};
}
if (diagnostic.code === DiagnosticsProvider_1.DiagnosticCode.MODIFIERS_CANNOT_APPEAR_HERE) {
return {
...diagnostic,
message: diagnostic.message +
'\nIf this is a declare statement, move it into <script context="module">..</script>'
};
}
return diagnostic;
}
/**
* Due to source mapping, some ranges may be swapped: Start is end. Swap back in this case.
*/
function swapDiagRangeStartEndIfNecessary(diag) {
diag.range = (0, utils_1.swapRangeStartEndIfNecessary)(diag.range);
return diag;
}
/**
* Checks if diagnostic is not within a section that should be completely ignored
* because it's purely generated.
*/
function isNotGenerated(text) {
return (diagnostic) => {
return !(0, utils_2.isInGeneratedCode)(text, diagnostic.pos, diagnostic.end);
};
}
function isUnusedReactiveStatementLabel(tsApiModule, project, diagnostic) {
if (diagnostic.code !== DiagnosticsProvider_1.DiagnosticCode.UNUSED_LABEL) {
return false;
}
const sourceFile = diagnostic.fileName && project.program.getSourceFile(diagnostic.fileName);
if (!sourceFile) {
return false;
}
const diagNode = findDiagnosticNode(tsApiModule, diagnostic, sourceFile);
if (!diagNode) {
return false;
}
// TS warning targets the identifier
if (!tsApiModule.isIdentifier(diagNode)) {
return false;
}
if (!diagNode.parent) {
return false;
}
return (0, utils_4.isReactiveStatement)(tsApiModule, diagNode.parent);
}
/**
* Checks if diagnostics should be ignored because they report an unused expression* in
* a reactive statement, and those actually have side effects in Svelte (hinting deps).
*
* $: x, update()
*
* Only `let` (i.e. reactive) variables are ignored. For the others, new diagnostics are
* emitted, centered on the (non reactive) identifiers in the initial warning.
*/
function resolveNoopsInReactiveStatements(tsAstModule, project, diagnostics) {
const notLet = (node) => {
const declaration = project.checker.getSymbolAtLocation(node)?.valueDeclaration;
if (!declaration || declaration.kind !== tsAstModule.SyntaxKind.VariableDeclaration) {
return true;
}
const declarationNode = declaration.resolve(project);
if (!declarationNode || !tsAstModule.isVariableDeclarationList(declarationNode.parent)) {
return true;
}
return (declarationNode.parent.flags & tsAstModule.NodeFlags.Let) === 0;
};
const expandRemainingNoopWarnings = (diagnostic) => {
const { code, fileName } = diagnostic;
// guard: not target error
const isNoopDiag = code === DiagnosticsProvider_1.DiagnosticCode.NOOP_IN_COMMAS;
if (!isNoopDiag) {
return;
}
const sourceFile = fileName && project.program.getSourceFile(fileName);
if (!sourceFile) {
return;
}
const diagNode = findDiagnosticNode(tsAstModule, diagnostic, sourceFile);
if (!diagNode) {
return;
}
if (!(0, utils_4.isInReactiveStatement)(tsAstModule, diagNode)) {
return;
}
const copyWorker = copyDiagnosticAndChangeNode(tsAstModule, diagnostic, sourceFile);
return (
// for all identifiers in diagnostic node
(0, utils_4.gatherIdentifiers)(tsAstModule, diagNode)
// ignore `let` (i.e. reactive) variables
.filter(notLet)
// and create targeted diagnostics just for the remaining ids
.map(copyWorker));
};
const expandedDiagnostics = (0, utils_1.passMap)(diagnostics, expandRemainingNoopWarnings).flat();
return expandedDiagnostics.length === diagnostics.length
? expandedDiagnostics
: // This can generate duplicate diagnostics
expandedDiagnostics.filter(dedupDiagnostics());
}
function dedupDiagnostics() {
const hashDiagnostic = (diag) => [diag.pos, diag.end, diag.category, diag.fileName, diag.code]
.map((x) => JSON.stringify(x))
.join(':');
const known = new Set();
return (diag) => {
const key = hashDiagnostic(diag);
if (known.has(key)) {
return false;
}
else {
known.add(key);
return true;
}
};
}
function get$$PropsAliasForInfo(tsAstModule, tsApiModule, project, get$$PropsDefWithCache, document) {
if (!/type\s+\$\$Props[\s\n]+=/.test(document.getText())) {
return;
}
const propsDef = get$$PropsDefWithCache();
if (!propsDef || !tsAstModule.isTypeAliasDeclaration(propsDef)) {
return;
}
const type = project.checker.getTypeAtLocation(propsDef.name);
if (!type) {
return;
}
// TS says symbol is always defined but it's not
// TODO no API for getting the aliased symbol?
// const rootSymbolName = (type.aliasSymbol ?? type.symbol)?.name;
const rootSymbol = type.getSymbol();
if (!rootSymbol) {
return;
}
if (rootSymbol.flags & tsApiModule.SymbolFlags.TypeLiteral) {
const node = rootSymbol.declarations?.[0]?.resolve(project);
if (!node || !tsAstModule.isTypeAliasDeclaration(node.parent) || !node.parent.name) {
return;
}
return [node.parent.name.text, propsDef];
}
const rootSymbolName = type.getSymbol()?.name;
if (!rootSymbolName) {
return;
}
return [rootSymbolName, propsDef];
}
function get$$PropsDef(tsAstModule, project, snapshot) {
const program = project.program;
const sourceFile = program.getSourceFile(toVirtualPath(snapshot));
if (!program || !sourceFile) {
return undefined;
}
const renderFunction = sourceFile.statements.find((statement) => tsAstModule.isFunctionDeclaration(statement) &&
statement.name?.text === svelte2tsx_1.internalHelpers.renderName);
return renderFunction?.body?.statements.find((node) => (tsAstModule.isTypeAliasDeclaration(node) ||
tsAstModule.isInterfaceDeclaration(node)) &&
node.name.text === '$$Props');
}
function movePropsErrorRangeBackIfNecessary(tsAstModule, diagnostic, snapshot, get$$PropsDefWithCache, get$$PropsAliasForWithCache) {
const possibly$$PropsError = (0, utils_2.isAfterSvelte2TsxPropsReturn)(snapshot.getFullText(), snapshot.offsetAt(diagnostic.range.start));
if (!possibly$$PropsError) {
return;
}
if (diagnostic.message.includes('$$Props')) {
const propsDef = get$$PropsDefWithCache();
if (!propsDef) {
return;
}
const generatedPropsStart = (0, utils_4.getStartOfNode)(tsAstModule, propsDef.name, propsDef.getSourceFile());
const propsStart = snapshot.getOriginalPosition(snapshot.positionAt(generatedPropsStart));
if (propsStart) {
return {
start: propsStart,
end: { ...propsStart, character: propsStart.character + '$$Props'.length }
};
}
return;
}
const aliasForInfo = get$$PropsAliasForWithCache();
if (!aliasForInfo) {
return;
}
const [aliasFor, propsDef] = aliasForInfo;
if (diagnostic.message.includes(aliasFor)) {
return (0, documents_1.mapRangeToOriginal)(snapshot, {
start: snapshot.positionAt((0, utils_4.getStartOfNode)(tsAstModule, propsDef.name, propsDef.getSourceFile())),
end: snapshot.positionAt(propsDef.name.end)
});
}
}
function expectedTransitionThirdArgument(tsAstModule, diagnostic, tsDoc, project) {
if (diagnostic.code !== DiagnosticsProvider_1.DiagnosticCode.EXPECTED_N_ARGUMENTS ||
!tsDoc.getText(0, diagnostic.pos).endsWith('__sveltets_2_ensureTransition(')) {
return false;
}
const sourceFile = diagnostic.fileName && project.program.getSourceFile(diagnostic.fileName);
if (!sourceFile) {
return false;
}
const node = findDiagnosticNode(tsAstModule, diagnostic, sourceFile);
if (!node || !tsAstModule.isIdentifier(node)) {
return false;
}
if (!node.parent || !tsAstModule.isCallExpression(node.parent)) {
return false;
}
const callExpression = node.parent;
const signature = callExpression && project.checker.getResolvedSignature(callExpression);
return (signature?.parameters.filter((parameter) => !(parameter.flags & typescript_1.default.SymbolFlags.Optional))
.length === 3);
}
function getDiagnosticTag(tsDiag) {
const tags = [];
if (tsDiag.reportsUnnecessary) {
tags.push(vscode_languageserver_1.DiagnosticTag.Unnecessary);
}
if (tsDiag.reportsDeprecated) {
tags.push(vscode_languageserver_1.DiagnosticTag.Deprecated);
}
return tags;
}
//# sourceMappingURL=DiagnosticsProvider.js.map