"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Linter = void 0;
const child_process_1 = require("child_process");
const LSP = require("vscode-languageserver/node");
const shebang_1 = require("../util/shebang");
const config_1 = require("./config");
const types_1 = require("./types");
const SUPPORTED_BASH_DIALECTS = ['sh', 'bash', 'dash', 'ksh'];
class Linter {
    constructor({ console, cwd, executablePath }) {
        this.executablePath = executablePath;
        this.cwd = cwd || process.cwd();
        this._canLint = true;
        this.console = console;
    }
    get canLint() {
        return this._canLint;
    }
    lint(document, folders, additionalShellCheckArguments = []) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this._canLint) {
                return { diagnostics: [], codeActions: [] };
            }
            const result = yield this.runShellCheck(document, folders, additionalShellCheckArguments);
            if (!this._canLint) {
                return { diagnostics: [], codeActions: [] };
            }
            return mapShellCheckResult({ document, result });
        });
    }
    runShellCheck(document, folders, additionalArgs = []) {
        return __awaiter(this, void 0, void 0, function* () {
            const documentText = document.getText();
            const { shellDialect } = (0, shebang_1.analyzeShebang)(documentText);
            // NOTE: that ShellCheck actually does shebang parsing, but we manually
            // do it here in order to fallback to bash. This enables parsing files
            // with a bash syntax.
            const shellName = shellDialect && SUPPORTED_BASH_DIALECTS.includes(shellDialect)
                ? shellDialect
                : 'bash';
            const workspaceFolderSourcePaths = folders
                // NOTE: on some system folder.name is undefined
                .map((folder) => { var _a; return (_a = folder === null || folder === void 0 ? void 0 : folder.name) === null || _a === void 0 ? void 0 : _a.trim(); })
                .filter((folderName) => folderName)
                .map((folderName) => `--source-path=${folderName}`);
            const args = [
                `--shell=${shellName}`,
                '--format=json1',
                '--external-sources',
                `--source-path=${this.cwd}`,
                ...workspaceFolderSourcePaths,
                ...additionalArgs,
            ];
            this.console.log(`ShellCheck: running "${this.executablePath} ${args.join(' ')}"`);
            let out = '';
            let err = '';
            const proc = new Promise((resolve, reject) => {
                const proc = (0, child_process_1.spawn)(this.executablePath, [...args, '-'], { cwd: this.cwd });
                proc.on('error', reject);
                proc.on('close', resolve);
                proc.stdout.on('data', (data) => (out += data));
                proc.stderr.on('data', (data) => (err += data));
                proc.stdin.on('error', () => {
                    // XXX: Ignore STDIN errors in case the process ends too quickly, before we try to
                    // write. If we write after the process ends without this, we get an uncatchable EPIPE.
                    // This is solved in Node >= 15.1 by the "on('spawn', ...)" event, but we need to
                    // support earlier versions.
                });
                proc.stdin.end(documentText);
            });
            // XXX: do we care about exit code? 0 means "ok", 1 possibly means "errors",
            // but the presence of parseable errors in the output is also sufficient to
            // distinguish.
            let exit;
            try {
                exit = yield proc;
            }
            catch (e) {
                // TODO: we could do this up front?
                if (e.code === 'ENOENT') {
                    // shellcheck path wasn't found, don't try to lint any more:
                    this.console.warn(`ShellCheck: disabling linting as no executable was found at path '${this.executablePath}'`);
                    this._canLint = false;
                    return { comments: [] };
                }
                throw new Error(`ShellCheck: failed with code ${exit}: ${e}\nout:\n${out}\nerr:\n${err}`);
            }
            let raw;
            try {
                raw = JSON.parse(out);
            }
            catch (e) {
                throw new Error(`ShellCheck: json parse failed with error ${e}\nout:\n${out}\nerr:\n${err}`);
            }
            return types_1.ShellCheckResultSchema.parse(raw);
        });
    }
}
exports.Linter = Linter;
function mapShellCheckResult({ document, result, }) {
    const diagnostics = [];
    const codeActions = [];
    for (const comment of result.comments) {
        const start = {
            line: comment.line - 1,
            character: comment.column - 1,
        };
        const end = {
            line: comment.endLine - 1,
            character: comment.endColumn - 1,
        };
        const diagnostic = {
            message: comment.message,
            severity: config_1.LEVEL_TO_SEVERITY[comment.level] || LSP.DiagnosticSeverity.Error,
            code: `SC${comment.code}`,
            source: 'shellcheck',
            range: LSP.Range.create(start, end),
            codeDescription: {
                href: `https://www.shellcheck.net/wiki/SC${comment.code}`,
            },
            tags: config_1.CODE_TO_TAGS[comment.code],
            // NOTE: we could use the 'data' property this enable easier fingerprinting
        };
        diagnostics.push(diagnostic);
        const codeAction = CodeActionProvider.getCodeAction({
            comment,
            document,
            diagnostics: [diagnostic],
        });
        if (codeAction) {
            codeActions.push(codeAction);
        }
    }
    return { diagnostics, codeActions };
}
/**
 * Code has been adopted from https://github.com/vscode-shellcheck/vscode-shellcheck/
 * and modified to fit the needs of this project.
 *
 * The MIT License (MIT)
 * Copyright (c) Timon Wong
 */
class CodeActionProvider {
    static getCodeAction({ comment, document, diagnostics, }) {
        const { code, fix } = comment;
        if (!fix || fix.replacements.length === 0) {
            return null;
        }
        const { replacements } = fix;
        if (replacements.length === 0) {
            return null;
        }
        const edits = this.getTextEdits(replacements);
        if (!edits.length) {
            return null;
        }
        return {
            title: `Apply fix for SC${code}`,
            diagnostics,
            edit: {
                changes: {
                    [document.uri]: edits,
                },
            },
            kind: LSP.CodeActionKind.QuickFix,
        };
    }
    static getTextEdits(replacements) {
        if (replacements.length === 1) {
            return [this.getTextEdit(replacements[0])];
        }
        else if (replacements.length === 2) {
            return [this.getTextEdit(replacements[1]), this.getTextEdit(replacements[0])];
        }
        return [];
    }
    static getTextEdit(replacement) {
        const startPos = LSP.Position.create(replacement.line - 1, replacement.column - 1);
        const endPos = LSP.Position.create(replacement.endLine - 1, replacement.endColumn - 1);
        return {
            range: LSP.Range.create(startPos, endPos),
            newText: replacement.replacement,
        };
    }
}
//# sourceMappingURL=index.js.map