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