UNPKG

svelte-language-server

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