"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.assertShellcheckResult = exports.Linter = void 0;
const child_process_1 = require("child_process");
const LSP = require("vscode-languageserver");
function formatMessage(comment) {
    return (comment.code ? `SC${comment.code}: ` : '') + comment.message;
}
class Linter {
    constructor({ console, cwd, executablePath }) {
        this.executablePath = executablePath;
        this.cwd = cwd || process.cwd();
        this._canLint = !!this.executablePath; // TODO: any reason to create a linter if the path is null?
        this.console = console;
    }
    get canLint() {
        return this._canLint;
    }
    lint(document, folders) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.executablePath || !this._canLint)
                return [];
            const result = yield this.runShellcheck(this.executablePath, document, folders);
            if (!this._canLint)
                return [];
            const diags = [];
            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,
                };
                diags.push({
                    message: formatMessage(comment),
                    severity: mapSeverity(comment.level),
                    code: comment.code,
                    source: 'shellcheck',
                    range: { start, end },
                });
            }
            return diags;
        });
    }
    runShellcheck(executablePath, document, folders) {
        return __awaiter(this, void 0, void 0, function* () {
            const args = [
                '--shell=bash',
                '--format=json1',
                '--external-sources',
                `--source-path=${this.cwd}`,
            ];
            for (const folder of folders) {
                args.push(`--source-path=${folder.name}`);
            }
            let out = '';
            let err = '';
            const proc = new Promise((resolve, reject) => {
                const proc = (0, child_process_1.spawn)(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(document.getText());
            });
            // 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) {
                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}`);
            }
            assertShellcheckResult(raw);
            return raw;
        });
    }
}
exports.Linter = Linter;
function assertShellcheckResult(val) {
    if (val !== null && typeof val !== 'object') {
        throw new Error(`shellcheck: unexpected json output ${typeof val}`);
    }
    if (!Array.isArray(val.comments)) {
        throw new Error(`shellcheck: unexpected json output: expected 'comments' array ${typeof val.comments}`);
    }
    for (const idx in val.comments) {
        const comment = val.comments[idx];
        if (comment !== null && typeof comment != 'object') {
            throw new Error(`shellcheck: expected comment at index ${idx} to be object, found ${typeof comment}`);
        }
        if (typeof comment.file !== 'string')
            throw new Error(`shellcheck: expected comment file at index ${idx} to be string, found ${typeof comment.file}`);
        if (typeof comment.level !== 'string')
            throw new Error(`shellcheck: expected comment level at index ${idx} to be string, found ${typeof comment.level}`);
        if (typeof comment.message !== 'string')
            throw new Error(`shellcheck: expected comment message at index ${idx} to be string, found ${typeof comment.level}`);
        if (typeof comment.line !== 'number')
            throw new Error(`shellcheck: expected comment line at index ${idx} to be number, found ${typeof comment.line}`);
        if (typeof comment.endLine !== 'number')
            throw new Error(`shellcheck: expected comment endLine at index ${idx} to be number, found ${typeof comment.endLine}`);
        if (typeof comment.column !== 'number')
            throw new Error(`shellcheck: expected comment column at index ${idx} to be number, found ${typeof comment.column}`);
        if (typeof comment.endColumn !== 'number')
            throw new Error(`shellcheck: expected comment endColumn at index ${idx} to be number, found ${typeof comment.endColumn}`);
        if (typeof comment.code !== 'number')
            throw new Error(`shellcheck: expected comment code at index ${idx} to be number, found ${typeof comment.code}`);
    }
}
exports.assertShellcheckResult = assertShellcheckResult;
const severityMapping = {
    error: LSP.DiagnosticSeverity.Error,
    warning: LSP.DiagnosticSeverity.Warning,
    info: LSP.DiagnosticSeverity.Information,
    style: LSP.DiagnosticSeverity.Hint,
};
// Severity mappings:
// https://github.com/koalaman/shellcheck/blob/364c33395e2f2d5500307f01989f70241c247d5a/src/ShellCheck/Formatter/Format.hs#L50
const mapSeverity = (sev) => severityMapping[sev] || LSP.DiagnosticSeverity.Error;
//# sourceMappingURL=linter.js.map