UNPKG

ember-template-lint

Version:
465 lines (389 loc) 14.4 kB
'use strict'; const path = require('path'); const chalk = require('chalk'); const findUp = require('find-up'); const micromatch = require('micromatch'); const resolve = require('resolve'); const defaultConfigurations = require('./config'); const DEPRECATED_RULES = require('./helpers/deprecated-rules'); const defaultRules = require('./rules'); const KNOWN_ROOT_PROPERTIES = new Set([ 'extends', 'rules', 'pending', 'ignore', 'plugins', 'overrides', ]); const SUPPORTED_OVERRIDE_KEYS = new Set(['files', 'rules']); // Severity level definitions const IGNORE_SEVERITY = 0; const WARNING_SEVERITY = 1; const ERROR_SEVERITY = 2; const CONFIG_FILE_NAME = '.template-lintrc.js'; const ALLOWED_ERROR_CODES = new Set([ // resolve package error codes 'MODULE_NOT_FOUND', // Yarn PnP Error Code 'QUALIFIED_PATH_RESOLUTION_FAILED', ]); function requirePlugin(workingDir, pluginName, fromConfigPath) { let basedir = fromConfigPath === undefined ? workingDir : path.dirname(fromConfigPath); // throws exception if not found let pluginPath = resolve.sync(pluginName, { basedir }); return require(pluginPath); // eslint-disable-line import/no-dynamic-require } function resolveProjectConfig(workingDir, options) { let configPath; if (options.configPath) { configPath = path.resolve(workingDir, options.configPath); try { // Making sure that the filePath exists, before requiring it directly this is // needed in order to ensure that we only squelch module missing errors for // the config path itself (and not other files that the config path may // require itself) require.resolve(configPath); } catch (error) { let isModuleMissingError = ALLOWED_ERROR_CODES.has(error.code); if (!isModuleMissingError) { throw error; } throw new Error( `The configuration file specified (${options.configPath}) could not be found. Aborting.` ); } } else if (options.configPath === false) { // we are explicitly using `--no-config-path` flag return {}; } else { configPath = findUp.sync(CONFIG_FILE_NAME); if (configPath === undefined) { // we weren't given an explicit --config-path argument, and we couldn't // find a relative .template-lintrc.js file, just use the default "empty" config return {}; } } options.resolvedConfigPath = configPath; return require(configPath); // eslint-disable-line import/no-dynamic-require } function forEachPluginConfiguration(plugins, callback) { if (!plugins) { return; } for (let pluginName in plugins) { let pluginConfigurations = plugins[pluginName].configurations; if (!pluginConfigurations) { continue; } for (let configurationName in pluginConfigurations) { let configuration = pluginConfigurations[configurationName]; callback(configuration, configurationName, pluginName); } } } function normalizeExtends(config, options) { let logger = options.console || console; let extendedList = []; if (config.extends) { if (typeof config.extends === 'string') { extendedList = [config.extends]; } else if (Array.isArray(config.extends)) { extendedList = config.extends.slice(); } else { logger.log(chalk.yellow('config.extends should be string or array ')); } } return extendedList; } function ensureRootProperties(config, source) { config.rules = Object.assign({}, source.rules || {}); config.pending = (source.pending || []).slice(); config.overrides = (source.overrides || []).slice(); config.ignore = (source.ignore || []).slice(); config.extends = source.extends; } function migrateRulesFromRoot(config, source, options) { let logger = options.console || console; let invalidKeysFound = []; for (let key in source) { if (!KNOWN_ROOT_PROPERTIES.has(key)) { invalidKeysFound.push(key); config.rules[key] = source[key]; } } if (invalidKeysFound.length > 0) { logger.log( chalk.yellow( ` Rule configuration has been moved into a \`rules\` property. Please update your \`${CONFIG_FILE_NAME}\` file. The following rules have been migrated to the \`rules\` property: ${invalidKeysFound}. ` ) ); } } function processPlugins(workingDir, plugins = [], options, checkForCircularReference) { let logger = options.console || console; let pluginsHash = {}; for (let plugin of plugins) { let pluginName; if (typeof plugin === 'string') { pluginName = plugin; // the second argument here should actually be the config file path for // the _currently being processed_ config file (not neccesarily the one // specified to the bin script) plugin = requirePlugin(workingDir, pluginName, options.resolvedConfigPath); } let errorMessage; if (typeof plugin === 'object') { if (plugin.name) { if (!checkForCircularReference || !checkForCircularReference[plugin.name]) { pluginsHash[plugin.name] = plugin; } } else if (pluginName) { errorMessage = `Plugin (${pluginName}) has not defined the plugin \`name\` property`; } else { errorMessage = 'Inline plugin object has not defined the plugin `name` property'; } } else if (pluginName) { errorMessage = `Plugin (${pluginName}) did not return a plain object`; } else { errorMessage = 'Inline plugin is not a plain object'; } if (errorMessage) { logger.log(chalk.yellow(errorMessage)); } } forEachPluginConfiguration(pluginsHash, (configuration) => { // process plugins recursively Object.assign( pluginsHash, processPlugins(workingDir, configuration.plugins, options, pluginsHash) ); }); return pluginsHash; } function processLoadedRules(workingDir, config, options) { let loadedRules; if (config.loadedRules) { loadedRules = config.loadedRules; } else { // load all the default rules in `ember-template-lint` loadedRules = Object.assign({}, defaultRules); } // load plugin rules for (let pluginName in config.plugins) { let pluginRules = config.plugins[pluginName].rules; if (pluginRules) { loadedRules = Object.assign(loadedRules, pluginRules); } } forEachPluginConfiguration(config.plugins, (configuration) => { let plugins = processPlugins(workingDir, configuration.plugins, options, config.plugins); // process plugins recursively processLoadedRules(workingDir, { plugins, loadedRules }); }); return loadedRules; } function processLoadedConfigurations(workingDir, config, options) { let loadedConfigurations; if (config.loadedConfigurations) { loadedConfigurations = config.loadedConfigurations; } else { // load all the default configurations in `ember-template-lint` loadedConfigurations = Object.assign({}, defaultConfigurations); } forEachPluginConfiguration(config.plugins, (configuration, configurationName, pluginName) => { let name = `${pluginName}:${configurationName}`; loadedConfigurations[name] = configuration; // load plugins recursively let plugins = processPlugins(workingDir, configuration.plugins, options, config.plugins); processLoadedConfigurations(workingDir, { plugins, loadedConfigurations }, options); }); return loadedConfigurations; } function processExtends(config, options) { let logger = options.console || console; let extendedList = normalizeExtends(config, options); if (extendedList) { for (const extendName of extendedList) { let configuration = config.loadedConfigurations[extendName]; if (configuration) { // ignore loops if (!configuration.loadedConfigurations) { configuration.loadedConfigurations = config.loadedConfigurations; // continue chaining `extends` from plugins until done processExtends(configuration, options); delete configuration.loadedConfigurations; if (configuration.rules) { config.rules = Object.assign({}, configuration.rules, config.rules); } else { logger.log(chalk.yellow(`Missing rules for extends: ${extendName}`)); } } } else { logger.log(chalk.yellow(`Cannot find configuration for extends: ${extendName}`)); } delete config.extends; } } } function processIgnores(config) { config.ignore = config.ignore.map((pattern) => micromatch.matcher(pattern)); } /** * we were passed a rule, add the rule being passed in, to the config. * ex: * rule:severity * no-implicit-this:["error", { "allow": ["some-helper"] }] */ function getRuleFromString(rule) { const indexOfSeparator = rule.indexOf(':') + 1; // we have to split based on the index of the first instance of the separator because the separator could exist in the second half of the rule const name = rule.substring(0, indexOfSeparator - 1); // eslint-disable-line unicorn/prefer-string-slice let ruleConfig = rule.substring(indexOfSeparator); // eslint-disable-line unicorn/prefer-string-slice if (ruleConfig.startsWith('[')) { try { ruleConfig = JSON.parse(ruleConfig); } catch { throw new Error(`Error parsing specified \`--rule\` config ${rule} as JSON.`); } } const config = determineRuleConfig(ruleConfig); return { name, config }; } function validateRules(rules, loadedRules, options) { let logger = options.console || console; let invalidKeysFound = []; let deprecatedNamesFound = []; for (let key in rules) { if (!loadedRules[key]) { const deprecation = DEPRECATED_RULES.get(key); if (deprecation) { deprecatedNamesFound.push({ oldName: key, newName: deprecation }); rules[deprecation] = rules[key]; delete rules[key]; } else { invalidKeysFound.push(key); } } } if (invalidKeysFound.length > 0) { logger.log(chalk.yellow(`Invalid rule configuration found: ${invalidKeysFound}`)); } if (deprecatedNamesFound.length > 0) { logger.log( chalk.yellow(`Deprecated rule names found: ${JSON.stringify(deprecatedNamesFound, null, 4)}`) ); } } function validateOverrides(config, options) { if (config.overrides) { config.overrides = config.overrides.map((overrideConfig) => { // If there are keys in the object which are not yet supported by overrides functionality, throw an error. let overrideKeys = Object.keys(overrideConfig); let containsValidKeys = overrideKeys.every((item) => SUPPORTED_OVERRIDE_KEYS.has(item)); if (!containsValidKeys) { throw new Error( 'Using `overrides` in `.template-lintrc.js` only supports `files` and `rules` sections. Please update your configuration.' ); } // clone a deep copy of the override config to ensure it is not mutated let clonedResult = JSON.parse(JSON.stringify(overrideConfig)); // TODO: this should be updated to avoid mutation (mutates for deprecated rules), and the mutation should be moved into `processRules` which is expected to return a new value validateRules(clonedResult.rules, config.loadedRules, options); clonedResult.rules = processRules(clonedResult); return clonedResult; }); } } function _determineConfigForSeverity(config) { switch (config) { case 'off': return { config: false, severity: IGNORE_SEVERITY }; case 'warn': return { config: true, severity: WARNING_SEVERITY }; case 'error': return { config: true, severity: ERROR_SEVERITY }; } } function determineRuleConfig(ruleData) { let ruleConfig = { severity: ruleData === false ? IGNORE_SEVERITY : ERROR_SEVERITY, config: ruleData, }; let severityConfig; // In case of {'no-implicit-this': 'off|warn|error'} if (typeof ruleData === 'string') { severityConfig = _determineConfigForSeverity(ruleData); if (severityConfig) { ruleConfig = severityConfig; } } else if (Array.isArray(ruleData)) { // array of severity and custom rule config let severity = ruleData[0]; severityConfig = _determineConfigForSeverity(severity); if (severityConfig) { ruleConfig.severity = severityConfig.severity; ruleConfig.config = ruleData[1]; } } return ruleConfig; } /** * Sets the severity and config on the rule object. * Eg: * {'no-implicit-this': 'warn'} -> {'no-implicit-this': { severity: 'warn', config: true }} * { 'no-implicit-this': [ 'warn', { allow: [ 'fooData' ] } ] } * would become: * { 'no-implicit-this': { severity: 'warn', config: 'allow': [ 'fooData' ] } } * { * 'some-rule': 'lol', // -> { severity: 2, config: 'lol' } * 'other-thing': ['wat', 'is', 'this'], // { severity: 2, config: ['wat', 'is', 'this'] } * 'hmm-thing-here': { zomg: 'lol' }, // -> { severity: 2, config: { zomg: 'lol' } } * 'another-thing-there': 'off', // -> { severity: 0, config: false } * } * @param {*} configData */ function processRules(config) { let processedRules = Object.assign({}, config.rules); for (let key in processedRules) { let ruleData = processedRules[key]; processedRules[key] = determineRuleConfig(ruleData); } return processedRules; } function getProjectConfig(workingDir, options) { let source = options.config || resolveProjectConfig(workingDir, options); let config; if (source._processed) { config = source; } else { // don't mutate a `require`d object, you'll have a bad time config = {}; ensureRootProperties(config, source); migrateRulesFromRoot(config, source, options); config.plugins = processPlugins(workingDir, source.plugins, options); config.loadedRules = processLoadedRules(workingDir, config, options); config.loadedConfigurations = processLoadedConfigurations(workingDir, config, options); processExtends(config, options); processIgnores(config); validateRules(config.rules, config.loadedRules, options); validateOverrides(config, options); config.rules = processRules(config); config._processed = true; } return config; } module.exports = { ERROR_SEVERITY, IGNORE_SEVERITY, WARNING_SEVERITY, getProjectConfig, getRuleFromString, resolveProjectConfig, determineRuleConfig, processRules, };