"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CPD_ENGINE_CONFIG_DESCRIPTION = exports.DEFAULT_CPD_ENGINE_CONFIG = exports.PMD_ENGINE_CONFIG_DESCRIPTION = exports.DEFAULT_PMD_ENGINE_CONFIG = exports.CPD_AVAILABLE_LANGUAGES = exports.PMD_AVAILABLE_LANGUAGES = void 0;
exports.validateAndNormalizePmdConfig = validateAndNormalizePmdConfig;
exports.validateAndNormalizeCpdConfig = validateAndNormalizeCpdConfig;
exports.makeUniqueCaseInsensitive = makeUniqueCaseInsensitive;
const code_analyzer_engine_api_1 = require("@salesforce/code-analyzer-engine-api");
const utils_1 = require("@salesforce/code-analyzer-engine-api/utils");
const messages_1 = require("./messages");
const node_path_1 = __importDefault(require("node:path"));
const constants_1 = require("./constants");
const node_fs_1 = __importDefault(require("node:fs"));
exports.PMD_AVAILABLE_LANGUAGES = Object.values(constants_1.Language).filter(l => l !== constants_1.Language.TYPESCRIPT); // Typescript is available in CPD but not PMD
exports.CPD_AVAILABLE_LANGUAGES = Object.values(constants_1.Language);
const DEFAULT_JAVA_COMMAND = 'java';
const FILE_EXT_PATTERN = /^[.][a-zA-Z0-9]+$/;
exports.DEFAULT_PMD_ENGINE_CONFIG = {
    java_command: DEFAULT_JAVA_COMMAND,
    rule_languages: exports.PMD_AVAILABLE_LANGUAGES, // hidden
    file_extensions: constants_1.DEFAULT_FILE_EXTENSIONS,
    java_classpath_entries: [],
    custom_rulesets: []
};
exports.PMD_ENGINE_CONFIG_DESCRIPTION = {
    overview: (0, messages_1.getMessage)('PmdConfigOverview'),
    fieldDescriptions: {
        java_command: {
            descriptionText: (0, messages_1.getMessage)('SharedConfigFieldDescription_java_command', constants_1.PMD_ENGINE_NAME),
            valueType: "string",
            defaultValue: null // Using null for doc and since it indicates that the value is calculated based on the environment
        },
        // rule_languages - is excluded here so that it can remain hidden
        // rule_languages WILL REMAIN HIDDEN UNTIL A USER REQUESTS THIS - ALL LANGUAGES ARE ENABLED BY DEFAULT SO THERE MAY NOT BE A USE CASE FOR THIS YET
        file_extensions: {
            descriptionText: (0, messages_1.getMessage)('SharedConfigFieldDescription_file_extensions'),
            valueType: "object",
            defaultValue: exports.DEFAULT_PMD_ENGINE_CONFIG.file_extensions,
        },
        java_classpath_entries: {
            descriptionText: (0, messages_1.getMessage)('PmdConfigFieldDescription_java_classpath_entries'),
            valueType: "array",
            defaultValue: exports.DEFAULT_PMD_ENGINE_CONFIG.java_classpath_entries
        },
        custom_rulesets: {
            descriptionText: (0, messages_1.getMessage)('PmdConfigFieldDescription_custom_rulesets'),
            valueType: "array",
            defaultValue: exports.DEFAULT_PMD_ENGINE_CONFIG.custom_rulesets
        }
    }
};
const DEFAULT_MINIMUM_TOKENS = 100;
exports.DEFAULT_CPD_ENGINE_CONFIG = {
    java_command: DEFAULT_JAVA_COMMAND,
    rule_languages: exports.CPD_AVAILABLE_LANGUAGES, // hidden
    file_extensions: constants_1.DEFAULT_FILE_EXTENSIONS,
    minimum_tokens: exports.CPD_AVAILABLE_LANGUAGES.reduce((obj, lang) => {
        obj[lang] = DEFAULT_MINIMUM_TOKENS;
        return obj;
    }, {}),
    skip_duplicate_files: false
};
exports.CPD_ENGINE_CONFIG_DESCRIPTION = {
    overview: (0, messages_1.getMessage)('CpdConfigOverview'),
    fieldDescriptions: {
        java_command: {
            descriptionText: (0, messages_1.getMessage)('SharedConfigFieldDescription_java_command', constants_1.CPD_ENGINE_NAME),
            valueType: "string",
            defaultValue: null // Using null for doc and since it indicates that the value is calculated based on the environment
        },
        // rule_languages - is excluded here so that it can remain hidden
        // rule_languages WILL REMAIN HIDDEN UNTIL A USER REQUESTS THIS - ALL LANGUAGES ARE ENABLED BY DEFAULT SO THERE MAY NOT BE A USE CASE FOR THIS YET
        file_extensions: {
            descriptionText: (0, messages_1.getMessage)('SharedConfigFieldDescription_file_extensions'),
            valueType: "object",
            defaultValue: exports.DEFAULT_CPD_ENGINE_CONFIG.file_extensions,
        },
        minimum_tokens: {
            descriptionText: (0, messages_1.getMessage)('CpdConfigFieldDescription_minimum_tokens'),
            valueType: "object",
            defaultValue: exports.DEFAULT_CPD_ENGINE_CONFIG.minimum_tokens,
        },
        skip_duplicate_files: {
            descriptionText: (0, messages_1.getMessage)('CpdConfigFieldDescription_skip_duplicate_files'),
            valueType: "boolean",
            defaultValue: exports.DEFAULT_CPD_ENGINE_CONFIG.skip_duplicate_files,
        }
    }
};
async function validateAndNormalizePmdConfig(configValueExtractor, javaVersionIdentifier) {
    configValueExtractor.addKeysThatBypassValidation(['rule_languages']); // because rule_languages is currently hidden
    configValueExtractor.validateContainsOnlySpecifiedKeys(['file_extensions', 'java_command', 'java_classpath_entries', 'custom_rulesets']);
    const pmdConfigValueExtractor = new PmdConfigValueExtractor(configValueExtractor, javaVersionIdentifier);
    const javaClasspathEntries = pmdConfigValueExtractor.extractJavaClasspathEntries();
    return {
        java_command: await pmdConfigValueExtractor.extractJavaCommand(),
        rule_languages: pmdConfigValueExtractor.extractRuleLanguages(),
        file_extensions: pmdConfigValueExtractor.extractFileExtensions(),
        java_classpath_entries: pmdConfigValueExtractor.extractJavaClasspathEntries(),
        custom_rulesets: pmdConfigValueExtractor.extractCustomRulesets(javaClasspathEntries)
    };
}
async function validateAndNormalizeCpdConfig(configValueExtractor, javaVersionIdentifier) {
    configValueExtractor.addKeysThatBypassValidation(['rule_languages']); // because rule_languages is currently hidden
    configValueExtractor.validateContainsOnlySpecifiedKeys(['file_extensions', 'java_command', 'minimum_tokens', 'skip_duplicate_files']);
    const cpdConfigValueExtractor = new CpdConfigValueExtractor(configValueExtractor, javaVersionIdentifier);
    return {
        java_command: await cpdConfigValueExtractor.extractJavaCommand(),
        rule_languages: cpdConfigValueExtractor.extractRuleLanguages(),
        file_extensions: cpdConfigValueExtractor.extractFileExtensions(),
        minimum_tokens: cpdConfigValueExtractor.extractMinimumTokens(),
        skip_duplicate_files: cpdConfigValueExtractor.extractSkipDuplicateFiles()
    };
}
// Abstract class to extraction code for shared fields for PMD and CPD engines, like 'java_command' and 'rule_languages'.
class SharedConfigValueExtractor {
    configValueExtractor;
    javaVersionIdentifier;
    constructor(configValueExtractor, javaVersionIdentifier) {
        this.configValueExtractor = configValueExtractor;
        this.javaVersionIdentifier = javaVersionIdentifier;
    }
    async extractJavaCommand() {
        const javaCommand = this.configValueExtractor.extractString('java_command');
        if (!javaCommand) {
            return await this.attemptToAutoDetectJavaCommand();
        }
        try {
            await this.validateJavaCommandContainsValidVersion(javaCommand);
        }
        catch (err) {
            throw new Error((0, messages_1.getMessage)('InvalidUserSpecifiedJavaCommand', this.configValueExtractor.getFieldPath('java_command'), err.message));
        }
        return javaCommand;
    }
    async attemptToAutoDetectJavaCommand() {
        const commandsToAttempt = [
            // Environment variables specifying JAVA HOME take precedence (if they exist)
            ...['JAVA_HOME', 'JRE_HOME', 'JDK_HOME'].filter(v => process.env[v]) // only keep vars that have a non-empty defined value
                .map(v => node_path_1.default.join(process.env[v], 'bin', 'java')),
            // Attempt to just use the default java command that might be already on the path as a last attempt
            DEFAULT_JAVA_COMMAND
        ];
        const errorMessages = [];
        for (const possibleJavaCommand of commandsToAttempt) {
            try {
                // Yes we want to have an await statement in a loop in this case since we want to try one at a time
                await this.validateJavaCommandContainsValidVersion(possibleJavaCommand);
                return possibleJavaCommand;
            }
            catch (err) {
                errorMessages.push(err.message);
            }
        }
        const consolidatedErrorMessages = errorMessages.map((msg, idx) => (0, utils_1.indent)(`Attempt ${idx + 1}:\n${(0, utils_1.indent)(msg)}`, '  | ')).join('\n');
        throw new Error((0, messages_1.getMessage)('CouldNotLocateJava', constants_1.MINIMUM_JAVA_VERSION, consolidatedErrorMessages, this.configValueExtractor.getFieldPath('java_command'), this.configValueExtractor.getFieldPath('disable_engine')));
    }
    async validateJavaCommandContainsValidVersion(javaCommand) {
        let version;
        try {
            version = await this.javaVersionIdentifier.identifyJavaVersion(javaCommand);
        }
        catch (err) {
            /* istanbul ignore next */
            const errMsg = err instanceof Error ? err.message : String(err);
            throw new Error((0, messages_1.getMessage)('JavaVersionCheckProducedError', javaCommand, (0, utils_1.indent)(errMsg, '  | ')));
        }
        if (!version) {
            throw new Error((0, messages_1.getMessage)('UnrecognizableJavaVersion', javaCommand));
        }
        else if (version.compare(constants_1.MINIMUM_JAVA_VERSION) < 0) {
            throw new Error((0, messages_1.getMessage)('JavaBelowMinimumVersion', javaCommand, version.toString(), constants_1.MINIMUM_JAVA_VERSION));
        }
    }
    extractRuleLanguages() {
        let ruleLanguages = this.configValueExtractor.extractArray('rule_languages', code_analyzer_engine_api_1.ValueValidator.validateString);
        if (ruleLanguages === undefined) {
            return this.getDefaultRuleLanguages();
        }
        // Make unique and convert to lowercase
        const ruleLanguagesSet = new Set(ruleLanguages.map(l => l.toLowerCase()));
        // Provide support for 'ecmascript' which is a supported alias of 'javascript'
        if (ruleLanguagesSet.has('ecmascript')) {
            ruleLanguagesSet.delete('ecmascript');
            ruleLanguagesSet.add('javascript');
        }
        // Validate each language is supported
        ruleLanguages = [...ruleLanguagesSet];
        for (const ruleLanguage of ruleLanguages) {
            if (!this.getAllPossibleRuleLanguages().includes(ruleLanguage)) {
                throw new Error((0, messages_1.getMessage)('InvalidRuleLanguage', this.configValueExtractor.getFieldPath('rule_languages'), ruleLanguage, toAvailableLanguagesText(this.getAllPossibleRuleLanguages())));
            }
        }
        return ruleLanguages.sort();
    }
    extractFileExtensions() {
        const fileExtensionsExtractor = this.configValueExtractor.extractObjectAsExtractor('file_extensions', constants_1.DEFAULT_FILE_EXTENSIONS);
        fileExtensionsExtractor.validateContainsOnlySpecifiedKeys(Object.keys(constants_1.DEFAULT_FILE_EXTENSIONS));
        const extToLangMap = new Map(); // To keep track if file extension shows up with more than one language
        const fileExtensionsMap = { ...constants_1.DEFAULT_FILE_EXTENSIONS }; // Start with copy
        for (const language of Object.keys(fileExtensionsMap)) {
            const fileExts = makeUniqueCaseInsensitive(fileExtensionsExtractor.extractArray(language, (element, elementFieldPath) => code_analyzer_engine_api_1.ValueValidator.validateString(element, elementFieldPath, FILE_EXT_PATTERN), constants_1.DEFAULT_FILE_EXTENSIONS[language]));
            // Validate that none of the file extensions already exist in another language
            for (const fileExt of fileExts.map(fileExt => fileExt.toLowerCase())) {
                if (extToLangMap.has(fileExt) && extToLangMap.get(fileExt) !== language) {
                    throw new Error((0, messages_1.getMessage)('InvalidFileExtensionDueToItBeingListedTwice', fileExtensionsExtractor.getFieldPath(), fileExt, JSON.stringify([extToLangMap.get(fileExt), language])));
                }
                extToLangMap.set(fileExt, language);
            }
            fileExtensionsMap[language] = fileExts;
        }
        return fileExtensionsMap;
    }
}
class PmdConfigValueExtractor extends SharedConfigValueExtractor {
    constructor(configValueExtractor, javaVersionIdentifier) {
        super(configValueExtractor, javaVersionIdentifier);
    }
    extractJavaClasspathEntries() {
        const entries = this.configValueExtractor.extractArray('java_classpath_entries', (entry, entryField) => code_analyzer_engine_api_1.ValueValidator.validatePath(entry, entryField, [this.configValueExtractor.getConfigRoot()]), exports.DEFAULT_PMD_ENGINE_CONFIG.java_classpath_entries);
        for (let i = 0; i < entries.length; i++) {
            if (node_path_1.default.extname(entries[i]).toLowerCase() !== '.jar' && !node_fs_1.default.statSync(entries[i]).isDirectory()) {
                throw new Error((0, messages_1.getMessage)('InvalidJavaClasspathEntry', this.configValueExtractor.getFieldPath('java_classpath_entries') + `[${i}]`));
            }
        }
        return [...new Set(entries)]; // Converting to set and back to array to remove duplicates
    }
    extractCustomRulesets(javaClassPathEntries) {
        const customRulesets = this.configValueExtractor.extractArray('custom_rulesets', code_analyzer_engine_api_1.ValueValidator.validateString, exports.DEFAULT_PMD_ENGINE_CONFIG.custom_rulesets);
        const possibleParentFolders = [
            this.configValueExtractor.getConfigRoot(),
            ...javaClassPathEntries.filter(entry => !entry.toLowerCase().endsWith('.jar')) // folder based entries
        ];
        for (let i = 0; i < customRulesets.length; i++) {
            try {
                // Using the validatePath method to simply help resolve relative paths on disk to absolute paths on disk
                customRulesets[i] = code_analyzer_engine_api_1.ValueValidator.validatePath(customRulesets[i], this.configValueExtractor.getFieldPath('custom_rulesets') + `[${i}]`, [this.configValueExtractor.getConfigRoot(), ...possibleParentFolders]);
            }
            catch (_err) {
                // If the ruleset isn't found on disk, then at this point we could ideally attempt to validate that it
                // exists in one of the jar files, but that would be expensive. Instead, we will rely on our pmd-wrapper
                // (which is called when describing rules) to finish validation when we add in the ruleset on the Java
                // side - at which point our try/catch statement will provide a nice error message.
            }
        }
        return [...new Set(customRulesets)]; // Converting to set and back to array to remove duplicates
    }
    getAllPossibleRuleLanguages() {
        return exports.PMD_AVAILABLE_LANGUAGES;
    }
    getDefaultRuleLanguages() {
        return exports.DEFAULT_PMD_ENGINE_CONFIG.rule_languages;
    }
}
class CpdConfigValueExtractor extends SharedConfigValueExtractor {
    constructor(configValueExtractor, javaVersionIdentifier) {
        super(configValueExtractor, javaVersionIdentifier);
    }
    getAllPossibleRuleLanguages() {
        return exports.CPD_AVAILABLE_LANGUAGES;
    }
    getDefaultRuleLanguages() {
        return exports.DEFAULT_CPD_ENGINE_CONFIG.rule_languages;
    }
    extractMinimumTokens() {
        const minimumTokensExtractor = this.configValueExtractor.extractObjectAsExtractor('minimum_tokens', exports.DEFAULT_CPD_ENGINE_CONFIG.minimum_tokens);
        minimumTokensExtractor.validateContainsOnlySpecifiedKeys(exports.CPD_AVAILABLE_LANGUAGES);
        const minimumTokensMap = { ...exports.DEFAULT_CPD_ENGINE_CONFIG.minimum_tokens }; // Start with copy
        for (const language of exports.CPD_AVAILABLE_LANGUAGES) {
            const minimumTokensValue = minimumTokensExtractor.extractNumber(language, exports.DEFAULT_CPD_ENGINE_CONFIG.minimum_tokens[language]);
            if (minimumTokensValue <= 0 || Math.floor(minimumTokensValue) != minimumTokensValue) {
                throw new Error((0, messages_1.getMessage)('InvalidPositiveInteger', minimumTokensExtractor.getFieldPath(language)));
            }
            minimumTokensMap[language] = minimumTokensValue;
        }
        return minimumTokensMap;
    }
    extractSkipDuplicateFiles() {
        return this.configValueExtractor.extractBoolean('skip_duplicate_files', exports.DEFAULT_CPD_ENGINE_CONFIG.skip_duplicate_files);
    }
}
function toAvailableLanguagesText(languages) {
    return languages.map(lang => `'${lang}'`)
        .join(', ').replace(`'javascript'`, `'javascript' (or 'ecmascript')`);
}
function makeUniqueCaseInsensitive(arr) {
    const seen = new Set();
    return arr.filter((str) => {
        const lowerStr = str.toLowerCase();
        if (seen.has(lowerStr)) {
            return false;
        }
        seen.add(lowerStr);
        return true;
    });
}
//# sourceMappingURL=config.js.map