svelte-language-server
Version:
A language server for Svelte
461 lines • 22.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DiagnosticsProviderImpl = exports.DiagnosticCode = void 0;
const typescript_1 = __importDefault(require("typescript"));
const vscode_languageserver_1 = require("vscode-languageserver");
const documents_1 = require("../../../lib/documents");
const utils_1 = require("../utils");
const utils_2 = require("./utils");
const utils_3 = require("../../../utils");
const svelte_ast_utils_1 = require("../svelte-ast-utils");
const svelte2tsx_1 = require("svelte2tsx");
var DiagnosticCode;
(function (DiagnosticCode) {
DiagnosticCode[DiagnosticCode["MODIFIERS_CANNOT_APPEAR_HERE"] = 1184] = "MODIFIERS_CANNOT_APPEAR_HERE";
DiagnosticCode[DiagnosticCode["USED_BEFORE_ASSIGNED"] = 2454] = "USED_BEFORE_ASSIGNED";
DiagnosticCode[DiagnosticCode["JSX_ELEMENT_DOES_NOT_SUPPORT_ATTRIBUTES"] = 2607] = "JSX_ELEMENT_DOES_NOT_SUPPORT_ATTRIBUTES";
DiagnosticCode[DiagnosticCode["CANNOT_BE_USED_AS_JSX_COMPONENT"] = 2786] = "CANNOT_BE_USED_AS_JSX_COMPONENT";
DiagnosticCode[DiagnosticCode["NOOP_IN_COMMAS"] = 2695] = "NOOP_IN_COMMAS";
DiagnosticCode[DiagnosticCode["NEVER_READ"] = 6133] = "NEVER_READ";
DiagnosticCode[DiagnosticCode["ALL_IMPORTS_UNUSED"] = 6192] = "ALL_IMPORTS_UNUSED";
DiagnosticCode[DiagnosticCode["UNUSED_LABEL"] = 7028] = "UNUSED_LABEL";
DiagnosticCode[DiagnosticCode["DUPLICATED_JSX_ATTRIBUTES"] = 17001] = "DUPLICATED_JSX_ATTRIBUTES";
DiagnosticCode[DiagnosticCode["DUPLICATE_IDENTIFIER"] = 2300] = "DUPLICATE_IDENTIFIER";
DiagnosticCode[DiagnosticCode["MULTIPLE_PROPS_SAME_NAME"] = 1117] = "MULTIPLE_PROPS_SAME_NAME";
DiagnosticCode[DiagnosticCode["ARG_TYPE_X_NOT_ASSIGNABLE_TO_TYPE_Y"] = 2345] = "ARG_TYPE_X_NOT_ASSIGNABLE_TO_TYPE_Y";
DiagnosticCode[DiagnosticCode["TYPE_X_NOT_ASSIGNABLE_TO_TYPE_Y"] = 2322] = "TYPE_X_NOT_ASSIGNABLE_TO_TYPE_Y";
DiagnosticCode[DiagnosticCode["TYPE_X_NOT_ASSIGNABLE_TO_TYPE_Y_DID_YOU_MEAN"] = 2820] = "TYPE_X_NOT_ASSIGNABLE_TO_TYPE_Y_DID_YOU_MEAN";
DiagnosticCode[DiagnosticCode["UNKNOWN_PROP"] = 2353] = "UNKNOWN_PROP";
DiagnosticCode[DiagnosticCode["MISSING_PROPS"] = 2739] = "MISSING_PROPS";
DiagnosticCode[DiagnosticCode["MISSING_PROP"] = 2741] = "MISSING_PROP";
DiagnosticCode[DiagnosticCode["NO_OVERLOAD_MATCHES_CALL"] = 2769] = "NO_OVERLOAD_MATCHES_CALL";
DiagnosticCode[DiagnosticCode["CANNOT_FIND_NAME"] = 2304] = "CANNOT_FIND_NAME";
DiagnosticCode[DiagnosticCode["CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y"] = 2552] = "CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y";
DiagnosticCode[DiagnosticCode["EXPECTED_N_ARGUMENTS"] = 2554] = "EXPECTED_N_ARGUMENTS";
DiagnosticCode[DiagnosticCode["DEPRECATED_SIGNATURE"] = 6387] = "DEPRECATED_SIGNATURE"; // The signature '..' of '..' is deprecated
})(DiagnosticCode || (exports.DiagnosticCode = DiagnosticCode = {}));
class DiagnosticsProviderImpl {
constructor(lsAndTsDocResolver, configManager) {
this.lsAndTsDocResolver = lsAndTsDocResolver;
this.configManager = configManager;
}
async getDiagnostics(document, cancellationToken) {
const { lang, tsDoc } = await this.getLSAndTSDoc(document);
if (['coffee', 'coffeescript'].includes(document.getLanguageAttribute('script')) ||
cancellationToken?.isCancellationRequested) {
return [];
}
const isTypescript = tsDoc.scriptKind === typescript_1.default.ScriptKind.TSX || tsDoc.scriptKind === typescript_1.default.ScriptKind.TS;
// Document preprocessing failed, show parser error instead
if (tsDoc.parserError) {
return [
{
range: tsDoc.parserError.range,
severity: vscode_languageserver_1.DiagnosticSeverity.Error,
source: isTypescript ? 'ts' : 'js',
message: tsDoc.parserError.message,
code: tsDoc.parserError.code
}
];
}
let diagnostics = lang.getSyntacticDiagnostics(tsDoc.filePath);
const checkers = [lang.getSuggestionDiagnostics, lang.getSemanticDiagnostics];
for (const checker of checkers) {
if (cancellationToken) {
// wait a bit so the event loop can check for cancellation
// or let completion go first
await new Promise((resolve) => setTimeout(resolve, 10));
if (cancellationToken.isCancellationRequested) {
return [];
}
}
diagnostics.push(...checker.call(lang, tsDoc.filePath));
}
const additionalStoreDiagnostics = [];
const notGenerated = isNotGenerated(tsDoc.getFullText());
for (const diagnostic of diagnostics) {
if ((diagnostic.code === DiagnosticCode.NO_OVERLOAD_MATCHES_CALL ||
diagnostic.code === DiagnosticCode.ARG_TYPE_X_NOT_ASSIGNABLE_TO_TYPE_Y) &&
!notGenerated(diagnostic)) {
if ((0, utils_2.isStoreVariableIn$storeDeclaration)(tsDoc.getFullText(), diagnostic.start)) {
const storeName = tsDoc
.getFullText()
.substring(diagnostic.start, diagnostic.start + diagnostic.length);
const storeUsages = lang.findReferences(tsDoc.filePath, (0, utils_2.get$storeOffsetOf$storeDeclaration)(tsDoc.getFullText(), diagnostic.start))[0].references;
for (const storeUsage of storeUsages) {
additionalStoreDiagnostics.push({
...diagnostic,
messageText: `Cannot use '${storeName}' as a store. '${storeName}' needs to be an object with a subscribe method on it.\n\n${typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`,
start: storeUsage.textSpan.start,
length: storeUsage.textSpan.length
});
}
}
}
}
diagnostics.push(...additionalStoreDiagnostics);
diagnostics = diagnostics
.filter(notGenerated)
.filter((0, utils_3.not)(isUnusedReactiveStatementLabel))
.filter((diagnostics) => !expectedTransitionThirdArgument(diagnostics, tsDoc, lang));
diagnostics = resolveNoopsInReactiveStatements(lang, diagnostics);
const mapRange = rangeMapper(tsDoc, document, lang);
const noFalsePositive = isNoFalsePositive(document, tsDoc);
const converted = [];
for (const tsDiag of diagnostics) {
let diagnostic = {
range: (0, utils_1.convertRange)(tsDoc, tsDiag),
severity: (0, utils_1.mapSeverity)(tsDiag.category),
source: isTypescript ? 'ts' : 'js',
message: typescript_1.default.flattenDiagnosticMessageText(tsDiag.messageText, '\n'),
code: tsDiag.code,
tags: (0, utils_1.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;
}
async getLSAndTSDoc(document) {
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
}
}
exports.DiagnosticsProviderImpl = DiagnosticsProviderImpl;
function moveBindingErrorMessage(tsDiag, tsDoc, diagnostic, document) {
if (tsDiag.code === DiagnosticCode.TYPE_X_NOT_ASSIGNABLE_TO_TYPE_Y &&
tsDiag.start &&
tsDoc.getText(tsDiag.start, tsDiag.start + tsDiag.length).endsWith('.$$bindings')) {
let node = tsDoc.svelteNodeAt(diagnostic.range.start);
while (node && node.type !== 'InlineComponent') {
node = node.parent;
}
if (node) {
let name = tsDoc.getText(tsDiag.start + tsDiag.length, tsDiag.start + tsDiag.length + 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(snapshot, document, lang) {
const get$$PropsDefWithCache = (0, utils_3.memoize)(() => get$$PropsDef(lang, snapshot));
const get$$PropsAliasInfoWithCache = (0, utils_3.memoize)(() => get$$PropsAliasForInfo(get$$PropsDefWithCache, lang, document));
return (diagnostic) => {
let range = (0, documents_1.mapRangeToOriginal)(snapshot, diagnostic.range);
if (range.start.line < 0) {
range =
movePropsErrorRangeBackIfNecessary(diagnostic, snapshot, get$$PropsDefWithCache, get$$PropsAliasInfoWithCache) ?? range;
}
if (([DiagnosticCode.MISSING_PROP, DiagnosticCode.MISSING_PROPS].includes(diagnostic.code) ||
(DiagnosticCode.TYPE_X_NOT_ASSIGNABLE_TO_TYPE_Y &&
diagnostic.message.includes("'Properties<"))) &&
!(0, utils_1.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(diagnostic) {
const { file, start, length } = diagnostic;
if (!file || !start || !length) {
return;
}
const span = { start, length };
return (0, utils_2.findNodeAtSpan)(file, span);
}
function copyDiagnosticAndChangeNode(diagnostic) {
return (node) => ({
...diagnostic,
start: node.getStart(),
length: node.getWidth()
});
}
/**
* 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 ([DiagnosticCode.MULTIPLE_PROPS_SAME_NAME, 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 === 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 !== DiagnosticCode.NEVER_READ &&
diagnostic.code !== 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 !== 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 === 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 === 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_3.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) => {
if (diagnostic.start === undefined || diagnostic.length === undefined) {
return true;
}
return !(0, utils_2.isInGeneratedCode)(text, diagnostic.start, diagnostic.start + diagnostic.length);
};
}
function isUnusedReactiveStatementLabel(diagnostic) {
if (diagnostic.code !== DiagnosticCode.UNUSED_LABEL) {
return false;
}
const diagNode = findDiagnosticNode(diagnostic);
if (!diagNode) {
return false;
}
// TS warning targets the identifier
if (!typescript_1.default.isIdentifier(diagNode)) {
return false;
}
if (!diagNode.parent) {
return false;
}
return (0, utils_2.isReactiveStatement)(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(lang, diagnostics) {
const isLet = (file) => (node) => {
const defs = lang.getDefinitionAtPosition(file.fileName, node.getStart());
return !!defs && defs.some((def) => def.fileName === file.fileName && def.kind === 'let');
};
const expandRemainingNoopWarnings = (diagnostic) => {
const { code, file } = diagnostic;
// guard: missing info
if (!file) {
return;
}
// guard: not target error
const isNoopDiag = code === DiagnosticCode.NOOP_IN_COMMAS;
if (!isNoopDiag) {
return;
}
const diagNode = findDiagnosticNode(diagnostic);
if (!diagNode) {
return;
}
if (!(0, utils_2.isInReactiveStatement)(diagNode)) {
return;
}
return (
// for all identifiers in diagnostic node
(0, utils_2.gatherIdentifiers)(diagNode)
// ignore `let` (i.e. reactive) variables
.filter((0, utils_3.not)(isLet(file)))
// and create targeted diagnostics just for the remaining ids
.map(copyDiagnosticAndChangeNode(diagnostic)));
};
const expandedDiagnostics = (0, utils_3.flatten)((0, utils_3.passMap)(diagnostics, expandRemainingNoopWarnings));
return expandedDiagnostics.length === diagnostics.length
? expandedDiagnostics
: // This can generate duplicate diagnostics
expandedDiagnostics.filter(dedupDiagnostics());
}
function dedupDiagnostics() {
const hashDiagnostic = (diag) => [diag.start, diag.length, diag.category, diag.source, 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(get$$PropsDefWithCache, lang, document) {
if (!/type\s+\$\$Props[\s\n]+=/.test(document.getText())) {
return;
}
const propsDef = get$$PropsDefWithCache();
if (!propsDef || !typescript_1.default.isTypeAliasDeclaration(propsDef)) {
return;
}
const type = lang.getProgram()?.getTypeChecker()?.getTypeAtLocation(propsDef.name);
if (!type) {
return;
}
// TS says symbol is always defined but it's not
const rootSymbolName = (type.aliasSymbol ?? type.symbol)?.name;
if (!rootSymbolName) {
return;
}
return [rootSymbolName, propsDef];
}
function get$$PropsDef(lang, snapshot) {
const program = lang.getProgram();
const sourceFile = program?.getSourceFile(snapshot.filePath);
if (!program || !sourceFile) {
return undefined;
}
const renderFunction = sourceFile.statements.find((statement) => typescript_1.default.isFunctionDeclaration(statement) &&
statement.name?.getText() === svelte2tsx_1.internalHelpers.renderName);
return renderFunction?.body?.statements.find((node) => (typescript_1.default.isTypeAliasDeclaration(node) || typescript_1.default.isInterfaceDeclaration(node)) &&
node.name.getText() === '$$Props');
}
function movePropsErrorRangeBackIfNecessary(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();
const generatedPropsStart = propsDef?.name.getStart();
const propsStart = generatedPropsStart != null &&
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(propsDef.name.getStart()),
end: snapshot.positionAt(propsDef.name.getEnd())
});
}
}
function expectedTransitionThirdArgument(diagnostic, tsDoc, lang) {
if (diagnostic.code !== DiagnosticCode.EXPECTED_N_ARGUMENTS ||
!diagnostic.start ||
!tsDoc.getText(0, diagnostic.start).endsWith('__sveltets_2_ensureTransition(')) {
return false;
}
const node = findDiagnosticNode(diagnostic);
if (!node) {
return false;
}
// in TypeScript 5.4 the error is on the function name
// in earlier versions it's on the whole call expression
const callExpression = typescript_1.default.isIdentifier(node) && typescript_1.default.isCallExpression(node.parent)
? node.parent
: (0, utils_2.findNodeAtSpan)(node, { start: node.getStart(), length: node.getWidth() }, typescript_1.default.isCallExpression);
const signature = callExpression && lang.getProgram()?.getTypeChecker().getResolvedSignature(callExpression);
return (signature?.parameters.filter((parameter) => !(parameter.flags & typescript_1.default.SymbolFlags.Optional))
.length === 3);
}
//# sourceMappingURL=DiagnosticsProvider.js.map