UNPKG

svelte-language-server

Version:
461 lines 22.4 kB
"use strict"; 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