UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

186 lines (185 loc) 10.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FishDiagnostic = void 0; exports.getDiagnostics = getDiagnostics; const tree_sitter_1 = require("../utils/tree-sitter"); const node_types_1 = require("./node-types"); const errorCodes_1 = require("./errorCodes"); const file_operations_1 = require("../utils/file-operations"); 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"); var FishDiagnostic; (function (FishDiagnostic) { function create(code, node) { return { ...errorCodes_1.ErrorCodes.codes[code], range: { start: { line: node.startPosition.row, character: node.startPosition.column }, end: { line: node.endPosition.row, character: node.endPosition.column }, }, data: { node, }, }; } FishDiagnostic.create = create; })(FishDiagnostic || (exports.FishDiagnostic = FishDiagnostic = {})); function getDiagnostics(root, doc) { let diagnostics = []; const handler = new comments_handler_1.DiagnosticCommentsHandler(); const isAutoloadedFunctionName = (0, translation_1.isAutoloadedUriLoadsFunctionName)(doc); const docType = doc.getAutoloadType(); const autoloadedFunctions = []; const topLevelFunctions = []; const functionsWithReservedKeyword = []; const localFunctions = []; const localFunctionCalls = []; const commandNames = []; // compute in single pass for (const node of (0, tree_sitter_1.getChildNodes)(root)) { handler.handleNode(node); if (node.isError) { const found = (0, node_types_1.findErrorCause)(node.children); if (found && handler.isCodeEnabled(errorCodes_1.ErrorCodes.missingEnd)) { diagnostics.push(FishDiagnostic.create(errorCodes_1.ErrorCodes.missingEnd, found)); } } if ((0, node_types_1.isExtraEnd)(node) && handler.isCodeEnabled(errorCodes_1.ErrorCodes.extraEnd)) { diagnostics.push(FishDiagnostic.create(errorCodes_1.ErrorCodes.extraEnd, node)); } if ((0, node_types_1.isZeroIndex)(node) && handler.isCodeEnabled(errorCodes_1.ErrorCodes.missingEnd)) { diagnostics.push(FishDiagnostic.create(errorCodes_1.ErrorCodes.zeroIndexedArray, node)); } if ((0, node_types_1.isSingleQuoteVariableExpansion)(node) && handler.isCodeEnabled(errorCodes_1.ErrorCodes.singleQuoteVariableExpansion)) { diagnostics.push(FishDiagnostic.create(errorCodes_1.ErrorCodes.singleQuoteVariableExpansion, node)); } if ((0, node_types_1.isAlias)(node) && handler.isCodeEnabled(errorCodes_1.ErrorCodes.usedAlias)) { diagnostics.push(FishDiagnostic.create(errorCodes_1.ErrorCodes.usedAlias, node)); } if ((0, node_types_1.isUniversalDefinition)(node) && !doc.uri.split('/').includes('conf.d') && handler.isCodeEnabled(errorCodes_1.ErrorCodes.usedUnviersalDefinition)) { diagnostics.push(FishDiagnostic.create(errorCodes_1.ErrorCodes.usedUnviersalDefinition, node)); } if ((0, node_types_1.isSourceFilename)(node) && node.type !== 'subshell' && node.text.includes('/') && !file_operations_1.SyncFileHelper.exists(node.text) && handler.isCodeEnabled(errorCodes_1.ErrorCodes.sourceFileDoesNotExist)) { diagnostics.push(FishDiagnostic.create(errorCodes_1.ErrorCodes.sourceFileDoesNotExist, node)); } if ((0, node_types_1.isTestCommandVariableExpansionWithoutString)(node) && handler.isCodeEnabled(errorCodes_1.ErrorCodes.testCommandMissingStringCharacters)) { diagnostics.push(FishDiagnostic.create(errorCodes_1.ErrorCodes.testCommandMissingStringCharacters, node)); } if ((0, node_types_1.isConditionalWithoutQuietCommand)(node) && handler.isCodeEnabled(errorCodes_1.ErrorCodes.missingQuietOption)) { logger_1.logger.log('isSingleQuoteDiagnostic', { 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(errorCodes_1.ErrorCodes.missingQuietOption, node), range, }); } if ((0, node_types_1.isArgparseWithoutEndStdin)(node) && handler.isCodeEnabled(errorCodes_1.ErrorCodes.argparseMissingEndStdin)) { diagnostics.push(FishDiagnostic.create(errorCodes_1.ErrorCodes.argparseMissingEndStdin, node)); } if ((0, node_types_1.isVariableDefinitionWithExpansionCharacter)(node) && handler.isCodeEnabled(errorCodes_1.ErrorCodes.expansionInDefinition)) { diagnostics.push(FishDiagnostic.create(errorCodes_1.ErrorCodes.expansionInDefinition, node)); } /** store any functions we see, to reuse later */ 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); } /** keep this section at end of loop iteration, because it uses continue */ if ((0, node_types_2.isCommandName)(node)) commandNames.push(node); if (docType === 'completions') { // skip comments and options if ((0, node_types_2.isComment)(node) || (0, node_types_2.isOption)(node)) continue; // get the parent and previous sibling, for the next checks const { parent, previousSibling } = node; if (!parent || !previousSibling) continue; // skip if no parent command (note we already added commands above) if (!(0, node_types_2.isCommandWithName)(parent, 'complete')) continue; // skip if no previous sibling (since we're looking for `complete -n/-a/-c <HERE>`) if ((0, node_types_1.isMatchingCompleteOptionIsCommand)(previousSibling)) { // if we find a string, remove unnecessary tokens from arguments if ((0, node_types_2.isString)(node)) { // like this example: `(cmd; and cmd2)` // we remove the characters: `( ; and )` localFunctionCalls.push({ node, text: node.text.slice(1, -1) .replace(/[\(\)]/g, '') // Remove parentheses .replace(/[^\u0020-\u007F]/g, ''), // Keep only ASCII printable chars }); continue; } // otherwise, just add the node as is (should just be an unquoted command) localFunctionCalls.push({ node, text: node.text }); } } } const isMissingAutoloadedFunction = docType === 'functions' ? autoloadedFunctions.length === 0 : false; const isMissingAutoloadedFunctionButContainsOtherFunctions = isMissingAutoloadedFunction && topLevelFunctions.length > 0; // no function definition for autoloaded function file if (isMissingAutoloadedFunction && topLevelFunctions.length === 0) { diagnostics.push(FishDiagnostic.create(errorCodes_1.ErrorCodes.autoloadedFunctionMissingDefinition, root)); } // has functions/file.fish has top level functions, but none match the filename if (isMissingAutoloadedFunctionButContainsOtherFunctions) { topLevelFunctions.forEach(node => { diagnostics.push(FishDiagnostic.create(errorCodes_1.ErrorCodes.autoloadedFunctionFilenameMismatch, node)); }); } // has functions with invalid names -- (reserved keywords) functionsWithReservedKeyword.forEach(node => { diagnostics.push(FishDiagnostic.create(errorCodes_1.ErrorCodes.functionNameUsingReservedKeyword, node)); }); localFunctions.forEach(node => { const matches = commandNames.filter(call => call.text === node.text); if (matches.length === 0) return; if (!localFunctionCalls.some(call => call.node.equals(node))) { localFunctionCalls.push({ node, text: node.text }); } }); const unusedLocalFunction = localFunctions.filter(localFunction => { const callableRange = (0, tree_sitter_1.getRange)((0, tree_sitter_1.findEnclosingScope)(localFunction)); return !localFunctionCalls.find(call => { const callRange = (0, tree_sitter_1.getRange)((0, tree_sitter_1.findEnclosingScope)(call.node)); return (0, tree_sitter_1.containsRange)(callRange, callableRange) && call.text.split(/[&<>;|! ]/) .filter(cmd => !['or', 'and', 'not'].includes(cmd)) .some(t => t === localFunction.text); }); }); if (unusedLocalFunction.length > 1) { unusedLocalFunction.forEach(node => { diagnostics.push(FishDiagnostic.create(errorCodes_1.ErrorCodes.unusedLocalFunction, node)); }); } if (config_1.config.fish_lsp_diagnostic_disable_error_codes.length > 0) { for (const errorCode of config_1.config.fish_lsp_diagnostic_disable_error_codes) { diagnostics = diagnostics.filter(diagnostic => diagnostic.code !== errorCode); } } return diagnostics; }