"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PmdEngine = void 0;
const code_analyzer_engine_api_1 = require("@salesforce/code-analyzer-engine-api");
const utils_1 = require("@salesforce/code-analyzer-engine-api/utils");
const utils_2 = require("./utils");
const node_path_1 = __importDefault(require("node:path"));
const fs = __importStar(require("node:fs/promises"));
const constants_1 = require("./constants");
const pmd_wrapper_1 = require("./pmd-wrapper");
const messages_1 = require("./messages");
const pmd_rule_mappings_1 = require("./pmd-rule-mappings");
class PmdEngine extends code_analyzer_engine_api_1.Engine {
    pmdWrapperInvoker;
    config;
    extensionToLanguageMap;
    workspaceLiaisonCache = new Map();
    pmdRuleInfoListCache = new Map();
    constructor(config) {
        super();
        const javaCommandExecutor = new utils_1.JavaCommandExecutor(config.java_command, this.emitLogEvent.bind(this));
        const userProvidedJavaClasspathEntries = config.java_classpath_entries;
        this.pmdWrapperInvoker = new pmd_wrapper_1.PmdWrapperInvoker(javaCommandExecutor, userProvidedJavaClasspathEntries, this.emitLogEvent.bind(this));
        this.config = config;
        this.extensionToLanguageMap = (0, utils_2.toExtensionsToLanguageMap)(config.file_extensions);
    }
    getName() {
        return constants_1.PMD_ENGINE_NAME;
    }
    async getEngineVersion() {
        const pathToPackageJson = node_path_1.default.join(__dirname, '..', 'package.json');
        const packageJson = JSON.parse(await fs.readFile(pathToPackageJson, 'utf-8'));
        return packageJson.version;
    }
    async describeRules(describeOptions) {
        const workspaceLiaison = this.getWorkspaceLiaison(describeOptions.workspace);
        this.emitDescribeRulesProgressEvent(5);
        const ruleInfoList = await this.getPmdRuleInfoList(workspaceLiaison, describeOptions.workingFolder, (innerPerc) => this.emitDescribeRulesProgressEvent(5 + 90 * (innerPerc / 100))); // 5 to 95%
        const ruleDescriptions = ruleInfoList.map(toRuleDescription);
        ruleDescriptions.sort((rd1, rd2) => rd1.name.localeCompare(rd2.name));
        this.emitDescribeRulesProgressEvent(100);
        return ruleDescriptions;
    }
    async runRules(ruleNames, runOptions) {
        const workspaceLiaison = this.getWorkspaceLiaison(runOptions.workspace);
        const relevantLanguageToFilesMap = await workspaceLiaison.getRelevantLanguageToFilesMap();
        this.emitRunRulesProgressEvent(2);
        const ruleInfoList = await this.getPmdRuleInfoList(workspaceLiaison, runOptions.workingFolder, (innerPerc) => this.emitRunRulesProgressEvent(2 + 3 * (innerPerc / 100))); // 2 to 5%
        const selectedRuleInfoList = ruleNames
            .map(ruleName => fetchRuleInfoByRuleName(ruleInfoList, ruleName))
            .filter(ruleInfo => ruleInfo !== null);
        const runDataPerLanguage = {};
        const relevantLanguageIds = new Set(selectedRuleInfoList.map(ruleInfo => ruleInfo.languageId));
        for (const languageId of relevantLanguageIds) {
            const filesToScanForLanguage = relevantLanguageToFilesMap.get(toLanguageEnum(languageId)) || /* istanbul ignore next */ [];
            if (filesToScanForLanguage.length > 0) {
                runDataPerLanguage[languageId] = {
                    filesToScan: filesToScanForLanguage
                };
            }
        }
        if (Object.keys(runDataPerLanguage).length === 0) {
            this.emitRunRulesProgressEvent(100);
            return { violations: [] };
        }
        const pmdResults = await this.pmdWrapperInvoker.invokeRunCommand(selectedRuleInfoList, runDataPerLanguage, runOptions.workingFolder, (innerPerc) => this.emitRunRulesProgressEvent(5 + 93 * (innerPerc / 100))); // 5 to 98%
        const violations = [];
        for (const pmdViolation of pmdResults.violations) {
            violations.push(this.toViolation(pmdViolation));
        }
        for (const pmdProcessingError of pmdResults.processingErrors) {
            this.emitLogEvent(code_analyzer_engine_api_1.LogLevel.Error, (0, messages_1.getMessage)('ProcessingErrorForFile', 'PMD', pmdProcessingError.file, (0, utils_1.indent)(pmdProcessingError.message)));
        }
        this.emitRunRulesProgressEvent(100);
        return {
            violations: violations
        };
    }
    async getPmdRuleInfoList(workspaceLiaison, workingFolder, emitProgress) {
        const cacheKey = getCacheKey(workspaceLiaison.getWorkspace());
        if (!this.pmdRuleInfoListCache.has(cacheKey)) {
            const relevantLanguages = await workspaceLiaison.getRelevantLanguages();
            const pmdRuleLanguageIds = relevantLanguages.map(toPmdLanguageId);
            const allCustomRulesets = [
                ...constants_1.SFCA_RULESETS_TO_MAKE_AVAILABLE, // Our custom rulesets
                ...this.config.custom_rulesets // The user's custom rulesets
            ];
            const ruleInfoList = relevantLanguages.length === 0 ? [] :
                await this.pmdWrapperInvoker.invokeDescribeCommand(allCustomRulesets, pmdRuleLanguageIds, workingFolder, emitProgress);
            this.pmdRuleInfoListCache.set(cacheKey, ruleInfoList);
        }
        return this.pmdRuleInfoListCache.get(cacheKey);
    }
    getWorkspaceLiaison(workspace) {
        const cacheKey = getCacheKey(workspace);
        if (!this.workspaceLiaisonCache.has(cacheKey)) {
            this.workspaceLiaisonCache.set(cacheKey, new utils_2.WorkspaceLiaison(workspace, this.config.rule_languages, this.extensionToLanguageMap));
        }
        return this.workspaceLiaisonCache.get(cacheKey);
    }
    toViolation(pmdViolation) {
        const fileExt = node_path_1.default.extname(pmdViolation.codeLocation.file).toLowerCase();
        const language = this.extensionToLanguageMap.get(fileExt);
        return {
            ruleName: toUniqueRuleName(pmdViolation.rule, language),
            message: pmdViolation.message,
            codeLocations: [{
                    file: pmdViolation.codeLocation.file,
                    startLine: pmdViolation.codeLocation.startLine,
                    startColumn: pmdViolation.codeLocation.startCol,
                    endLine: pmdViolation.codeLocation.endLine,
                    endColumn: pmdViolation.codeLocation.endCol
                }],
            primaryLocationIndex: 0
        };
    }
}
exports.PmdEngine = PmdEngine;
function toRuleDescription(pmdRuleInfo) {
    const language = toLanguageEnum(pmdRuleInfo.languageId);
    const uniqueRuleName = toUniqueRuleName(pmdRuleInfo.name, language);
    let severityLevel;
    let tags;
    const customRulesetNameTags = pmdRuleInfo.ruleSets.map(rs => rs.replaceAll(' ', '')).filter(tag => !Object.values(code_analyzer_engine_api_1.COMMON_TAGS.CATEGORIES).includes(tag) && !tag.startsWith("AppExchange_"));
    if (uniqueRuleName in pmd_rule_mappings_1.RULE_MAPPINGS) {
        // Only if a default PMD rule was not customized do we use the severity from the rule mappings
        severityLevel = customRulesetNameTags.length === 0 ? pmd_rule_mappings_1.RULE_MAPPINGS[uniqueRuleName].severity : toSeverityLevel(pmdRuleInfo.priority);
        // For the tags, we start with the overridden tags from the RULE_MAPPINGS and add in the customRulesetNameTags
        // since we want to allow users to reference the existing standard rules in their custom ruleset
        // files and still get the benefit of us adding in a tag for the name of their custom ruleset.
        tags = [...pmd_rule_mappings_1.RULE_MAPPINGS[uniqueRuleName].tags, ...customRulesetNameTags];
    }
    else { // Any rule we don't know about from our RULE_MAPPINGS must be a custom rule. Unit tests prevent otherwise.
        severityLevel = toSeverityLevel(pmdRuleInfo.priority);
        const languageTag = language.charAt(0).toUpperCase() + language.slice(1);
        tags = [code_analyzer_engine_api_1.COMMON_TAGS.RECOMMENDED, ...customRulesetNameTags, languageTag, code_analyzer_engine_api_1.COMMON_TAGS.CUSTOM];
    }
    return {
        name: uniqueRuleName,
        severityLevel: severityLevel,
        tags: tags,
        description: pmdRuleInfo.description,
        resourceUrls: pmdRuleInfo.externalInfoUrl ? [pmdRuleInfo.externalInfoUrl] : [] // TODO: Eventually we'll want to add in well architected links
    };
}
function toUniqueRuleName(ruleName, ruleLanguage) {
    if (ruleName in constants_1.SHARED_RULE_NAMES && ruleLanguage !== constants_1.Language.APEX) {
        return `${ruleName}-${ruleLanguage}`;
    }
    return ruleName;
}
/* istanbul ignore next */
function toSeverityLevel(pmdRulePriority) {
    if (pmdRulePriority === "High") {
        return code_analyzer_engine_api_1.SeverityLevel.High;
    }
    else if (pmdRulePriority === "Medium") {
        return code_analyzer_engine_api_1.SeverityLevel.Moderate;
    }
    else if (pmdRulePriority === "Low") {
        return code_analyzer_engine_api_1.SeverityLevel.Low;
        // The following don't seem to show up in the standard rules, but are documented as options available to users
    }
    else if (pmdRulePriority === "Medium High") {
        return code_analyzer_engine_api_1.SeverityLevel.Moderate;
    }
    else if (pmdRulePriority === "Medium Low") {
        return code_analyzer_engine_api_1.SeverityLevel.Low;
    }
    throw new Error("Unsupported priority: " + pmdRulePriority);
}
function getCacheKey(workspace) {
    return workspace ? workspace.getWorkspaceId() : process.cwd();
}
function fetchRuleInfoByRuleName(ruleInfoList, uniqueRuleName) {
    // Note that some pmd rule names that were shared among languages were converted to contain a "-<language>" suffix.
    // So we need to map these names (like "WhileLoopsMustUseBraces-javascript") back into "WhileLoopsMustUseBraces" for the "javascript" language.
    const langSeparator = uniqueRuleName.indexOf('-');
    let pmdRuleLanguage = undefined;
    let pmdRuleName = uniqueRuleName;
    if (langSeparator > 0) {
        pmdRuleName = uniqueRuleName.substring(0, langSeparator);
        pmdRuleLanguage = uniqueRuleName.substring(langSeparator + 1);
    }
    else if (pmdRuleName in constants_1.SHARED_RULE_NAMES) {
        // It is also important that if a shared name is given like "WhileLoopsMustUseBraces" that we explicitly look
        // for its default language (the first in the array) so we don't pick up the rules for the other shared languages
        // by mistake. That is, if "WhileLoopsMustUseBraces" is for java but "WhileLoopsMustUseBraces-javascript" is for
        // javascript, so we don't want to select the javascript rule here.
        pmdRuleLanguage = constants_1.SHARED_RULE_NAMES[pmdRuleName][0];
    }
    return ruleInfoList.find(ruleInfo => ruleInfo.name === pmdRuleName && (pmdRuleLanguage === undefined || toLanguageEnum(ruleInfo.languageId) === pmdRuleLanguage)) || null;
}
function toPmdLanguageId(language) {
    // We must convert 'javascript' to 'ecmascript' since PMD actually uses 'ecmascript' as the identifier instead of 'javascript'
    return language == constants_1.Language.JAVASCRIPT ? 'ecmascript' : language;
}
function toLanguageEnum(pmdRuleLanguage) {
    // We must convert 'ecmascript' back to 'javascript'
    return pmdRuleLanguage == 'ecmascript' ? constants_1.Language.JAVASCRIPT : pmdRuleLanguage;
}
//# sourceMappingURL=pmd-engine.js.map