UNPKG

npm-groovy-lint

Version:

Lint, format and auto-fix your Groovy / Jenkinsfile / Gradle files

285 lines (262 loc) 11.2 kB
// Configuration file management import Debug from "debug"; const debug = Debug("npm-groovy-lint"); import fs from "fs-extra"; import importFresh from "import-fresh"; import * as path from "path"; import stripJsonComments from "strip-json-comments"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const defaultConfigLintFileName = ".groovylintrc-recommended.json"; const allConfigLintFileName = ".groovylintrc-all.json"; const NPM_GROOVY_LINT_CONSTANTS = { CodeNarcVersion: "2.2.0", GroovyVersion: "3.0.9", }; const configLintFilenames = [ ".groovylintrc.json", ".groovylintrc.js", ".groovylintrc.cjs", ".groovylintrc.yml", ".groovylintrc.yaml", ".groovylintrc", "package.json", ]; const configExtensions = ["json", "js", "cjs", "yml", "yaml", "groovylintrc"]; const defaultConfigFormatFileName = ".groovylintrc-format.json"; const configFormatFilenames = [".groovylintrc-format.json", ".groovylintrc-format.js"]; let overriddenRules; // Load configuration from identified file, or find config file from a start path async function loadConfig(startPathOrFile, mode = "lint", sourcefilepath, fileNamesIn = []) { let fileNames = [...fileNamesIn]; // Load config let configUser = {}; let configFilePath; if (configExtensions.includes(startPathOrFile.split(".").pop()) && mode !== "format") { // Sent file name configFilePath = startPathOrFile; configUser = await loadConfigFromFile(startPathOrFile); } else if (startPathOrFile.match(/^[a-zA-Z\d-_]+$/) && mode !== "format") { // Sent string: find a corresponding file name fileNames = configExtensions.map((ext) => `.groovylintrc-${startPathOrFile}.${ext}`); configFilePath = await getConfigFileName(sourcefilepath || process.cwd(), sourcefilepath, fileNames, ""); configUser = await loadConfigFromFile(configFilePath); } else { // sent directory let defaultConfig = defaultConfigLintFileName; if (mode === "lint" && fileNames.length === 0) { fileNames = configLintFilenames; } else if (mode === "format") { fileNames = fileNames.length === 0 ? configFormatFilenames : fileNames; defaultConfig = defaultConfigFormatFileName; } configFilePath = await getConfigFileName(startPathOrFile, sourcefilepath, fileNames, defaultConfig); // Load user configuration from file configUser = await loadConfigFromFile(configFilePath); } // Complete PATH to codeNarc rulesets if defined in .groovylintrc if (configFilePath && configUser.codenarcRulesets) { // Set ruleSet file if found from config file configUser.rulesets = configUser.codenarcRulesets .split(",") .map((rulesetFile) => path.resolve(path.dirname(configFilePath) + "/" + rulesetFile)) .join(","); } // Shorten rule names if long rule names Cat.Rule replaced by Ru configUser.rules = await shortenRuleNames(configUser.rules || {}); // If config extends a standard one, merge it configUser = await manageExtends(configUser); // If mode = "format", call user defined rules to apply them upon the default formatting rules if (mode === "format") { const customUserConfig = await loadConfig(startPathOrFile, "lint", sourcefilepath, fileNamesIn); for (const ruleKey of Object.keys(customUserConfig.rules)) { if (configUser.rules[ruleKey]) { configUser.rules[ruleKey] = customUserConfig.rules[ruleKey]; } } } if (overriddenRules != null) { configUser.overriddenRules = overriddenRules; } return configUser; } // If extends defined, gather base level rules and append them to current rules async function manageExtends(configUser) { if (configUser.extends) { const baseConfigFilePath = await findConfigInPath(__dirname, [`.groovylintrc-${configUser.extends}.json`]); let baseConfig = await loadConfigFromFile(baseConfigFilePath); baseConfig.rules = await shortenRuleNames(baseConfig.rules || {}); // A config can extend another config that extends another config baseConfig = await manageExtends(baseConfig); // Delete doublons for (const baseRuleName of Object.keys(baseConfig.rules)) { for (const userRuleName of Object.keys(configUser.rules)) { if (baseRuleName === userRuleName) { delete baseConfig.rules[baseRuleName]; } } } configUser.rules = Object.assign(baseConfig.rules, configUser.rules); delete configUser.extends; } return configUser; } // Returns configuration filename async function getConfigFileName(startPathOrFile, sourcefilepath, fileNames = configLintFilenames, defaultConfig = defaultConfigLintFileName) { let configFilePath = null; // Find one of the config file formats are the root of the linted file (if source is sent with sourcefilepath) if ([".", process.cwd()].includes(startPathOrFile) && sourcefilepath) { try { const stat = await fs.lstat(sourcefilepath); const dir = stat.isDirectory() ? sourcefilepath : path.parse(sourcefilepath).dir; configFilePath = await findConfigInPath(dir, fileNames); } catch (e) { debug(`Unable to find config file for ${sourcefilepath} (${e.message})`); } } // Find one of the config file formats at the root of the project or at upper directory levels if (configFilePath == null) { try { const stat = await fs.lstat(startPathOrFile); const dir = stat.isDirectory ? startPathOrFile : path.parse(startPathOrFile).dir; configFilePath = await findConfigInPath(dir, fileNames); } catch (e) { debug(`Unable to find config file for ${sourcefilepath} (${e.message})`); } } // Custom file names: try to find matching file if (configFilePath == null && defaultConfig === "") { configFilePath = await findConfigInPath(__dirname, fileNames); } // If not found, use .groovylintrc-recommended.js delivered with npm-groovy-lint if (configFilePath == null) { configFilePath = await findConfigInPath(__dirname, [defaultConfig]); } configFilePath = path.resolve(configFilePath); debug(`GroovyLint used config file: ${configFilePath}`); if (!configExtensions.includes(configFilePath.split(".").pop())) { throw new Error(`Unable to find a configuration file ${startPathOrFile}`); } return configFilePath; } // try to find a config file or config prop in package.json async function findConfigInPath(directoryPath, configFilenamesIn) { for (const filename of configFilenamesIn) { const filePath = path.join(directoryPath, filename); if (await fs.exists(filePath)) { if (filename === "package.json") { try { await loadPackageJSONConfigFile(filePath); return filePath; } catch (error) { /* ignore */ debug("Error loading JSON config file: " + error.message); } } else { return filePath; } } } //if not found, try parent directory const parentPath = path.dirname(directoryPath); if (parentPath && parentPath !== directoryPath) { return await findConfigInPath(parentPath, configFilenamesIn); } return null; } // Load configuration depending of the file format async function loadConfigFromFile(filePath) { let configLoaded; switch (path.extname(filePath)) { case ".js": case ".cjs": configLoaded = await loadJSConfigFile(filePath); break; case ".json": if (path.basename(filePath) === "package.json") { configLoaded = await loadPackageJSONConfigFile(filePath); } else { configLoaded = await loadJSONConfigFile(filePath); } break; case ".yaml": case ".yml": configLoaded = await loadYAMLConfigFile(filePath); break; default: configLoaded = null; } if (configLoaded != null && !filePath.includes(defaultConfigLintFileName) && !filePath.includes(allConfigLintFileName)) { overriddenRules = configLoaded.rules; } return configLoaded; } // Javascript format async function loadJSConfigFile(filePath) { try { return importFresh(filePath); } catch (e) { debug(`Error reading JavaScript file: ${filePath}`); e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; throw e; } } // JSON format async function loadJSONConfigFile(filePath) { try { const fileContent = await fs.readFile(filePath); return JSON.parse(stripJsonComments(fileContent.toString())); } catch (e) { debug(`Error reading JSON file: ${filePath}`); e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; e.messageTemplate = "failed-to-read-json"; e.messageData = { path: filePath, message: e.message, }; throw e; } } // YAML format async function loadYAMLConfigFile(filePath) { // lazy load YAML to improve performance when not used const yaml = await import("js-yaml"); try { // empty YAML file can be null, so always use const fileContent = await readFile(filePath); return yaml.load(fileContent) || {}; } catch (e) { debug(`Error reading YAML file: ${filePath}`); e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; throw e; } } // json in package.json format async function loadPackageJSONConfigFile(filePath) { try { const packageData = await loadJSONConfigFile(filePath); if (!Object.hasOwnProperty.call(packageData, "groovylintConfig")) { throw Object.assign(new Error(`${filePath} doesn't have 'groovylintConfig' property`), { code: "GROOVYLINT_CONFIG_FIELD_NOT_FOUND" }); } return packageData.groovylintConfig; } catch (e) { debug(`Error reading package.json file: ${filePath}`); //e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; throw e; } } // Read file async function readFile(filePath) { const fileContent = await fs.readFile(filePath, "utf8"); return fileContent.replace(/^\ufeff/u, ""); } // Remove rule category of rule name if defined. Ex: "basic.ConstantAssertExpression" becomes "ConstantAssertExpression" async function shortenRuleNames(rules) { const shortenedRules = {}; for (const ruleName of Object.keys(rules)) { const ruleNameShort = ruleName.includes(".") ? ruleName.split(".")[1] : ruleName; shortenedRules[ruleNameShort] = rules[ruleName]; } return shortenedRules; } export { NPM_GROOVY_LINT_CONSTANTS, loadConfig, getConfigFileName, overriddenRules };