UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

308 lines (307 loc) 17.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FishDiagnostic = void 0; exports.getDiagnostics = getDiagnostics; const vscode_languageserver_1 = require("vscode-languageserver"); const tree_sitter_1 = require("../utils/tree-sitter"); const options_1 = require("../parsing/options"); const node_types_1 = require("./node-types"); const error_codes_1 = require("./error-codes"); const config_1 = require("../config"); const comments_handler_1 = require("./comments-handler"); const logger_1 = require("../logger"); const translation_1 = require("../utils/translation"); const node_types_2 = require("../utils/node-types"); const builtins_1 = require("../utils/builtins"); const no_execute_diagnostic_1 = require("./no-execute-diagnostic"); const invalid_error_code_1 = require("./invalid-error-code"); const analyze_1 = require("../analyze"); const unreachable_1 = require("../parsing/unreachable"); const references_1 = require("../references"); var FishDiagnostic; (function (FishDiagnostic) { function create(code, node, message = '') { const errorMessage = message && message.length > 0 ? error_codes_1.ErrorCodes.codes[code].message + ' | ' + message : error_codes_1.ErrorCodes.codes[code].message; return { ...error_codes_1.ErrorCodes.codes[code], range: { start: { line: node.startPosition.row, character: node.startPosition.column }, end: { line: node.endPosition.row, character: node.endPosition.column }, }, message: errorMessage, data: { node, fromSymbol: false, }, }; } FishDiagnostic.create = create; function fromDiagnostic(diagnostic) { return { ...diagnostic, data: { node: undefined, fromSymbol: false, }, }; } FishDiagnostic.fromDiagnostic = fromDiagnostic; function fromSymbol(code, symbol) { const diagnostic = create(code, symbol.focusedNode); if (code === error_codes_1.ErrorCodes.unusedLocalDefinition) { const localSymbolType = symbol.isVariable() ? 'variable' : 'function'; diagnostic.message += ` ${localSymbolType} '${symbol.name}' is defined but never used.`; } diagnostic.range = symbol.selectionRange; diagnostic.data.fromSymbol = true; return diagnostic; } FishDiagnostic.fromSymbol = fromSymbol; })(FishDiagnostic || (exports.FishDiagnostic = FishDiagnostic = {})); function getDiagnostics(root, doc) { const diagnostics = []; const handler = new comments_handler_1.DiagnosticCommentsHandler(); const isAutoloadedFunctionName = (0, translation_1.isAutoloadedUriLoadsFunctionName)(doc); const docType = doc.getAutoloadType(); analyze_1.analyzer.ensureCachedDocument(doc); const allFunctions = analyze_1.analyzer.getFlatDocumentSymbols(doc.uri).filter(s => s.isFunction()); const autoloadedFunctions = []; const topLevelFunctions = []; const functionsWithReservedKeyword = []; const localFunctions = []; const localFunctionCalls = []; const commandNames = []; const completeCommandNames = []; const definedVariables = {}; const isFunctionWithEventHook = (0, node_types_1.isFunctionWithEventHookCallback)(doc, handler, allFunctions); for (const node of (0, tree_sitter_1.getChildNodes)(root)) { handler.handleNode(node); const invalidDiagnosticCodes = (0, invalid_error_code_1.checkForInvalidDiagnosticCodes)(node); if (invalidDiagnosticCodes.length > 0) { diagnostics.push(...invalidDiagnosticCodes); } if (node.type === 'variable_name' || node.text.startsWith('$') || (0, node_types_2.isString)(node)) { const parent = (0, node_types_2.findParentCommand)(node); if (parent && (0, node_types_2.isCommandWithName)(parent, 'set', 'test')) { const opt = (0, node_types_2.isCommandWithName)(parent, 'test') ? options_1.Option.short('-n') : options_1.Option.create('-q', '--query'); let text = (0, node_types_2.isString)(node) ? node.text.slice(1, -1) : node.text; if (text.startsWith('$')) text = text.slice(1); if (text && text.length !== 0) { const scope = (0, node_types_2.findParent)(node, n => (0, node_types_2.isScope)(n)); if (scope && parent.children.some(c => (0, options_1.isMatchingOption)(c, opt))) { definedVariables[text] = definedVariables[text] || []; definedVariables[text]?.push(scope); } } } } if (node.isError) { const found = (0, node_types_1.findErrorCause)(node.children); if (found && handler.isCodeEnabled(error_codes_1.ErrorCodes.missingEnd)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.missingEnd, node)); if (docType === 'conf.d') { return diagnostics; } } } if ((0, node_types_1.isExtraEnd)(node) && handler.isCodeEnabled(error_codes_1.ErrorCodes.extraEnd)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.extraEnd, node)); } if ((0, node_types_1.isZeroIndex)(node) && handler.isCodeEnabled(error_codes_1.ErrorCodes.missingEnd)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.zeroIndexedArray, node)); } if ((0, node_types_1.isSingleQuoteVariableExpansion)(node) && handler.isCodeEnabled(error_codes_1.ErrorCodes.singleQuoteVariableExpansion)) { if (doc.getAutoloadType() !== 'completions') { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.singleQuoteVariableExpansion, node)); } } if ((0, node_types_1.isAlias)(node) && handler.isCodeEnabled(error_codes_1.ErrorCodes.usedAlias)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.usedAlias, node)); } if ((0, node_types_1.isUniversalDefinition)(node) && docType !== 'conf.d' && handler.isCodeEnabled(error_codes_1.ErrorCodes.usedUnviersalDefinition)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.usedUnviersalDefinition, node)); } if ((0, node_types_1.isSourceFilename)(node) && handler.isCodeEnabled(error_codes_1.ErrorCodes.sourceFileDoesNotExist)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.sourceFileDoesNotExist, node)); } if ((0, node_types_1.isDotSourceCommand)(node) && handler.isCodeEnabled(error_codes_1.ErrorCodes.dotSourceCommand)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.dotSourceCommand, node)); } if ((0, node_types_1.isTestCommandVariableExpansionWithoutString)(node) && handler.isCodeEnabled(error_codes_1.ErrorCodes.testCommandMissingStringCharacters)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.testCommandMissingStringCharacters, node)); } if ((0, node_types_1.isConditionalWithoutQuietCommand)(node) && handler.isCodeEnabled(error_codes_1.ErrorCodes.missingQuietOption)) { logger_1.logger.log('isConditionalWithoutQuietCommand', { type: node.type, text: node.text }); const command = node.firstNamedChild || node; let subCommand = command; if (command.text.includes('string')) { subCommand = command.nextSibling || node.nextSibling; } const range = { start: { line: command.startPosition.row, character: command.startPosition.column }, end: { line: subCommand.endPosition.row, character: subCommand.endPosition.column }, }; diagnostics.push({ ...FishDiagnostic.create(error_codes_1.ErrorCodes.missingQuietOption, node), range, }); } if ((0, node_types_1.isArgparseWithoutEndStdin)(node) && handler.isCodeEnabled(error_codes_1.ErrorCodes.argparseMissingEndStdin)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.argparseMissingEndStdin, node)); } if ((0, node_types_1.isVariableDefinitionWithExpansionCharacter)(node, definedVariables) && handler.isCodeEnabled(error_codes_1.ErrorCodes.dereferencedDefinition)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.dereferencedDefinition, node)); } if ((0, node_types_1.isFishLspDeprecatedVariableName)(node) && handler.isCodeEnabled(error_codes_1.ErrorCodes.fishLspDeprecatedEnvName)) { logger_1.logger.log('isFishLspDeprecatedVariableName', doc.getText((0, tree_sitter_1.getRange)(node))); diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.fishLspDeprecatedEnvName, node, (0, node_types_1.getDeprecatedFishLspMessage)(node))); } if ((0, node_types_2.isFunctionDefinitionName)(node)) { if (isAutoloadedFunctionName(node)) autoloadedFunctions.push(node); if ((0, node_types_2.isTopLevelFunctionDefinition)(node)) topLevelFunctions.push(node); if ((0, builtins_1.isReservedKeyword)(node.text)) functionsWithReservedKeyword.push(node); if (!isAutoloadedFunctionName(node)) localFunctions.push(node); if (isFunctionWithEventHook(node)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.autoloadedFunctionWithEventHookUnused, node, `Function '${node.text}' has an event hook but is not called anywhere in the workspace.`)); } } if ((0, node_types_2.isComment)(node) || (0, node_types_2.isOption)(node)) continue; if ((0, node_types_2.isCommandName)(node)) commandNames.push(node); const { parent, previousSibling } = node; if (!parent || !previousSibling) continue; if ((0, node_types_2.isCommandWithName)(parent, 'abbr') && (0, node_types_1.isMatchingAbbrFunction)(previousSibling)) { localFunctionCalls.push({ node, text: node.text }); continue; } if ((0, node_types_2.isCommandWithName)(parent, 'bind')) { const subcommands = parent.children.slice(2).filter(c => !(0, node_types_2.isOption)(c)); subcommands.forEach(subcommand => { if ((0, node_types_2.isString)(subcommand)) { localFunctionCalls.push({ node, text: subcommand.text.slice(1, -1) .replace(/[\(\)]/g, '') .replace(/[^\u0020-\u007F]/g, ''), }); return; } localFunctionCalls.push({ node, text: subcommand.text }); }); continue; } if (doc.isAutoloadedWithPotentialCompletions()) { if ((0, node_types_2.isCompleteCommandName)(node)) completeCommandNames.push(node); if (!(0, node_types_2.isCommandWithName)(parent, 'complete')) continue; if ((0, node_types_1.isMatchingCompleteOptionIsCommand)(previousSibling)) { if ((0, node_types_2.isString)(node)) { localFunctionCalls.push({ node, text: node.text.slice(1, -1) .replace(/[\(\)]/g, '') .replace(/[^\u0020-\u007F]/g, ''), }); continue; } localFunctionCalls.push({ node, text: node.text }); } } } handler.finalizeStateMap(root.text.split('\n').length + 1); const isMissingAutoloadedFunction = docType === 'functions' ? autoloadedFunctions.length === 0 : false; const isMissingAutoloadedFunctionButContainsOtherFunctions = isMissingAutoloadedFunction && topLevelFunctions.length > 0; if (isMissingAutoloadedFunction && topLevelFunctions.length === 0 && handler.isCodeEnabledAtNode(error_codes_1.ErrorCodes.autoloadedFunctionMissingDefinition, root)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.autoloadedFunctionMissingDefinition, root)); } if (isMissingAutoloadedFunctionButContainsOtherFunctions) { topLevelFunctions.forEach(node => { if (handler.isCodeEnabledAtNode(error_codes_1.ErrorCodes.autoloadedFunctionFilenameMismatch, node)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.autoloadedFunctionFilenameMismatch, node)); } }); } functionsWithReservedKeyword.forEach(node => { if (handler.isCodeEnabledAtNode(error_codes_1.ErrorCodes.functionNameUsingReservedKeyword, node)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.functionNameUsingReservedKeyword, node)); } }); const duplicateFunctions = {}; allFunctions.forEach(node => { const currentDupes = duplicateFunctions[node.name] ?? []; currentDupes.push(node); duplicateFunctions[node.name] = currentDupes; }); Object.entries(duplicateFunctions).forEach(([_, functionSymbols]) => { if (functionSymbols.length <= 1) return; functionSymbols.forEach(n => { if (handler.isCodeEnabledAtNode(error_codes_1.ErrorCodes.duplicateFunctionDefinitionInSameScope, n.focusedNode)) { const dupes = functionSymbols.filter(s => s.scopeNode.equals(n.scopeNode) && !s.equals(n)) ?? []; if (dupes.length < 1) return; const diagnostic = FishDiagnostic.create(error_codes_1.ErrorCodes.duplicateFunctionDefinitionInSameScope, n.focusedNode); diagnostic.range = n.selectionRange; diagnostic.message += ` '${n.name}' is defined ${dupes.length + 1} time(s) in ${n.scopeTag.toUpperCase()} scope.`; diagnostic.message += `\n\nFILE: ${(0, translation_1.uriToReadablePath)(n.uri)}`; diagnostic.relatedInformation = dupes.filter(s => !s.equals(n)).map(s => vscode_languageserver_1.DiagnosticRelatedInformation.create(s.toLocation(), `${s.scopeTag.toUpperCase()} duplicate '${s.name}' defined on line ${s.focusedNode.startPosition.row}`)); diagnostics.push(diagnostic); } }); }); localFunctions.forEach(node => { const matches = commandNames.filter(call => call.text === node.text); if (matches.length === 0) return; if (!localFunctionCalls.some(call => call.text === node.text)) { localFunctionCalls.push({ node, text: node.text }); } }); const docNameMatchesCompleteCommandNames = completeCommandNames.some(node => node.text === doc.getAutoLoadName()); if (completeCommandNames.length > 0 && !docNameMatchesCompleteCommandNames && doc.isAutoloadedCompletion()) { const completeNames = new Set(); for (const completeCommandName of completeCommandNames) { if (!completeNames.has(completeCommandName.text) && handler.isCodeEnabledAtNode(error_codes_1.ErrorCodes.autoloadedCompletionMissingCommandName, completeCommandName)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.autoloadedCompletionMissingCommandName, completeCommandName, completeCommandName.text)); completeNames.add(completeCommandName.text); } } } if (handler.isCodeEnabled(error_codes_1.ErrorCodes.unusedLocalDefinition)) { const unusedLocalDefinitions = (0, references_1.allUnusedLocalReferences)(doc); for (const unusedLocalDefinition of unusedLocalDefinitions) { if (handler.isCodeEnabledAtNode(error_codes_1.ErrorCodes.unusedLocalDefinition, unusedLocalDefinition.focusedNode)) { diagnostics.push(FishDiagnostic.fromSymbol(error_codes_1.ErrorCodes.unusedLocalDefinition, unusedLocalDefinition)); } } } if (handler.isCodeEnabled(error_codes_1.ErrorCodes.unreachableCode)) { const unreachableNodes = (0, unreachable_1.findUnreachableCode)(root); for (const unreachableNode of unreachableNodes) { if (handler.isCodeEnabledAtNode(error_codes_1.ErrorCodes.unreachableCode, unreachableNode)) { diagnostics.push(FishDiagnostic.create(error_codes_1.ErrorCodes.unreachableCode, unreachableNode)); } } } if (config_1.config.fish_lsp_enable_experimental_diagnostics) { const noExecuteDiagnostics = (0, no_execute_diagnostic_1.getNoExecuteDiagnostics)(doc); for (const diagnostic of noExecuteDiagnostics) { if (handler.isCodeEnabledAtNode(error_codes_1.ErrorCodes.syntaxError, diagnostic.data.node)) { diagnostics.push(diagnostic); } } } return diagnostics; }