fish-lsp
Version:
LSP implementation for fish/fish-shell
308 lines (307 loc) • 17.3 kB
JavaScript
"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;
}