diff --git a/packages/sonarwhal/docs/user-guide/further-configuration/using-relative-resources.md b/packages/sonarwhal/docs/user-guide/further-configuration/using-relative-resources.md new file mode 100644 index 00000000000..e0d2b84f14f --- /dev/null +++ b/packages/sonarwhal/docs/user-guide/further-configuration/using-relative-resources.md @@ -0,0 +1,23 @@ +# Using relative resources + +You can use relative resources in your `.sonarwhalrc` file: + +```json +{ + "connector": { + "name": "../my-connector/connector.js" + }, + "formatters": ["../../formatters/my-formatter.js"], + "rules": { + "../my-rule/rule.js": "error" + }, + "rulesTimeout": 120000 +} +``` + +Some things to keep in mind: + +* Only relative paths to the config file are supported. +* Regardless of your OS you have to use `/` as the separator. +* The resource will be resolved relatively to where the config file that + needs the resource is. This is important if you are extending configurations. diff --git a/packages/sonarwhal/src/lib/config.ts b/packages/sonarwhal/src/lib/config.ts index 0d04c577443..de02fde6a5c 100644 --- a/packages/sonarwhal/src/lib/config.ts +++ b/packages/sonarwhal/src/lib/config.ts @@ -13,6 +13,7 @@ * ------------------------------------------------------------------------------ */ +import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; @@ -262,6 +263,93 @@ export class SonarwhalConfig { config = null; } + config = this.toAbsolutePaths(config, filePath); + + return config; + } + + /** + * Transforms any relative paths in the configuration to absolute using + * the value of `configPath`. `configPath` needs to be a folder. + * The values that can be changed are: + * * `connector`'s value: `{ "connector": "./myconnector" }` + * * `connector.name` value: `{ "connector": { "name": "./myconnector"} }` + * * `formatter`s and `parser`s values: `{ "formatters": ["./myformatter"] }` + * * `rule`s keys: `{ "rules: { "./myrule": "warning" } }` + */ + public static toAbsolutePaths(config: UserConfig, configRoot: string): UserConfig { + if (!config) { + return null; + } + + /* + * We could receive a path to a folder or a file. `dirname` will return different + * things depending on that. E.g.: + * * `path.dirname('/config/folder')` will return `/config` and we want `/config/folder` + * * `path.dirname('/config/folder/file')` will return `/config/folder` + * + * This is no good if we want to resolve relatively because we will get incorrect + * paths. To solve this we have to know if what we are receiving is a file or a + * folder and adjust accordingly. + */ + const stat = fs.statSync(configRoot); //eslint-disable-line + const configPath = stat.isDirectory() ? configRoot : path.dirname(configRoot); + + if (!configPath) { + return config; + } + + /** + * If `value` is a relative path (ie, it starts with `.`), it transforms it + * to an absolute path using the `configRoot` folder as the origin to `resolve`. + */ + const resolve = (value: string): string => { + if (!value.startsWith('.')) { + return value; + } + + return path.resolve(configPath, value); + }; + + // Update the connector value + if (config.connector) { + if (typeof config.connector === 'string') { + config.connector = resolve(config.connector); + } else { + config.connector.name = resolve(config.connector.name); + } + } + + // Update extends + if (config.extends) { + config.extends = config.extends.map(resolve); + } + + // Update formatters + if (config.formatters) { + config.formatters = config.formatters.map(resolve); + } + + // Update parsers + if (config.parsers) { + config.parsers = config.parsers.map(resolve); + } + + // Update rules + if (config.rules) { + const rules = Object.keys(config.rules); + + const transformedRules = rules.reduce((newRules, currentRule) => { + const newRule = resolve(currentRule); + + newRules[newRule] = config.rules[currentRule]; + + return newRules; + }, {}); + + config.rules = transformedRules; + } + return config; } diff --git a/packages/sonarwhal/src/lib/sonarwhal.ts b/packages/sonarwhal/src/lib/sonarwhal.ts index 58dba98fe48..1f71cfb9ef5 100644 --- a/packages/sonarwhal/src/lib/sonarwhal.ts +++ b/packages/sonarwhal/src/lib/sonarwhal.ts @@ -118,6 +118,49 @@ export class Sonarwhal extends EventEmitter { this.rules = new Map(); + /** + * Returns the configuration for a given rule ID. In the case of a rule + * pointing to a path, it will return the content of the first entry + * in the config that contains the ID. E.g.: + * * `../rule-x-content-type-options` is the key in the config + * * `x-content-type-options` is the ID of the rule + * * One of the keys in the config `includes` the rule ID so it's a match + * + * @param id The id of the rule + */ + const getRuleConfig = (id: string): RuleConfig | Array => { + if (config.rules[id]) { + return config.rules[id]; + } + + const ruleEntries = Object.keys(config.rules); + const idParts = id.split('/'); + + /** + * At this point we are trying to find the configuration of a rule specified + * via a path. + * The id of a rule (define in `Rule.meta.id`) will be `packageName/rule-id` + * but most likely the code is not going to be on a path that ends like that + * and will be more similar to `packageName/dist/src/rule-id.js` + * + * To solve this, we iterate over all the keys of the `rules` object until + * we find the first entry that includes all the parts of the id + * (`packageName` and `rule-id` in the example). + * + * E.g.: + * * `../rule-packageName/dist/src/rule-id.js` --> Passes + * * `../rule-packageAnotherName/dist/src/rule-id.js` --> Fails because + * `packageName` is not in that path + */ + const ruleKey = ruleEntries.find((entry) => { + return idParts.every((idPart) => { + return entry.includes(idPart); + }); + }); + + return config.rules[ruleKey]; + }; + resources.rules.forEach((Rule) => { debug('Loading rules'); const id = Rule.meta.id; @@ -130,7 +173,7 @@ export class Sonarwhal extends EventEmitter { ignoredConnectors.includes(connectorId); }; - const ruleOptions: RuleConfig | Array = config.rules[id]; + const ruleOptions: RuleConfig | Array = getRuleConfig(id); const severity: Severity = getSeverity(ruleOptions); if (ignoreRule(Rule)) { diff --git a/packages/sonarwhal/src/lib/utils/resource-loader.ts b/packages/sonarwhal/src/lib/utils/resource-loader.ts index e99a46d844d..31c9552a6d0 100644 --- a/packages/sonarwhal/src/lib/utils/resource-loader.ts +++ b/packages/sonarwhal/src/lib/utils/resource-loader.ts @@ -12,6 +12,7 @@ */ import * as path from 'path'; +import * as fs from 'fs'; import * as globby from 'globby'; import * as semver from 'semver'; @@ -244,11 +245,13 @@ const generateConfigPathsToResources = (configurations: Array, name: str */ export const loadResource = (name: string, type: ResourceType, configurations: Array = [], verifyVersion = false) => { debug(`Searching ${name}…`); - + const isSource = fs.existsSync(name); // eslint-disable-line no-sync const nameSplitted = name.split('/'); const packageName = nameSplitted[0]; - const resourceName = nameSplitted[1] || packageName; + const resourceName = isSource ? + name : + nameSplitted[1] || packageName; const key: string = `${type}-${name}`; @@ -269,13 +272,15 @@ export const loadResource = (name: string, type: ResourceType, configurations: A * has to be `rule-typescript-config/is-valid`. * But we need to load the package `@sonarwhal/rule-typescript-config`. */ - const sources: Array = [ - `@sonarwhal/${type}-${packageName}`, // Officially supported package - `sonarwhal-${type}-${packageName}`, // Third party package - path.normalize(`${SONARWHAL_ROOT}/dist/src/lib/${type}s/${packageName}/${packageName}.js`), // Part of core. E.g.: built-in formatters, parsers, connectors - path.normalize(currentProcessDir) // External rules. - // path.normalize(`${path.resolve(SONARWHAL_ROOT, '..')}/${key}`) // Things under `/packages/` for when we are developing something official. E.g.: `/packages/rule-http-cache` - ].concat(configPathsToResources); + const sources: Array = isSource ? + [path.resolve(currentProcessDir, name)] : // If the name is direct path to the source we should only check that + [ + `@sonarwhal/${type}-${packageName}`, // Officially supported package + `sonarwhal-${type}-${packageName}`, // Third party package + path.normalize(`${SONARWHAL_ROOT}/dist/src/lib/${type}s/${packageName}/${packageName}.js`), // Part of core. E.g.: built-in formatters, parsers, connectors + path.normalize(currentProcessDir) // External rules. + // path.normalize(`${path.resolve(SONARWHAL_ROOT, '..')}/${key}`) // Things under `/packages/` for when we are developing something official. E.g.: `/packages/rule-http-cache` + ].concat(configPathsToResources); let resource; let isValid: boolean = true; @@ -283,7 +288,14 @@ export const loadResource = (name: string, type: ResourceType, configurations: A sources.some((source: string) => { const res = getResource(source, type, resourceName); - if (res) { + if (res && isSource) { + isValid = true; + resource = res; + + return true; + } + + if (res && !isSource) { // Paths to sources might not have packages and versioning doesn't apply debug(`${name} found in ${source}`); if (source === currentProcessDir) { @@ -322,7 +334,12 @@ export const loadResource = (name: string, type: ResourceType, configurations: A throw new ResourceError(`Resource ${name} not found`, ResourceErrorStatus.NotFound); } - resources.set(key, resource); + if (isSource) { + resources.set(name, resource); + } else { + resources.set(key, resource); + } + return resource; }; diff --git a/packages/sonarwhal/src/lib/utils/rule-helpers.ts b/packages/sonarwhal/src/lib/utils/rule-helpers.ts index b1f028390af..96cec1a9212 100644 --- a/packages/sonarwhal/src/lib/utils/rule-helpers.ts +++ b/packages/sonarwhal/src/lib/utils/rule-helpers.ts @@ -1,4 +1,5 @@ -import * as path from 'path'; +import { basename, dirname, resolve } from 'path'; +import { findPackageRoot } from './misc'; /** Lower cases all the items of `list`. */ export const toLowerCase = (list: Array): Array => { @@ -26,32 +27,23 @@ export const getIncludedHeaders = (headers: object, headerList: Array = }; /** - * Returns the name of the rule based in the folder structure. - * - * * `/something/another` --> `` - * * `/something/rules/another/` --> `another` - * * `/something/rules/another` --> `another` - * * `/something/rules/rule-another` --> `another` - * * `/something/rule-another/` --> `another` - * * `/something/rule-another` --> `another` + * Returns the name of the rule based on: + * * if it is a single rule package --> Searches for the entry point in + * package.json + * * if it is muti rule package --> Searches the path to the rule that + * has the same name as the test file */ -export const getRuleName = (dirname: string, packageName?: string): string => { - const parts = dirname.split(path.sep); - let ruleName = ''; - - const normalize = (name) => { - return name.replace('rule-', ''); - }; +export const getRulePath = (name: string, multirule?: boolean): string => { + const dir = dirname(name); + const root = findPackageRoot(dir); - for (let i = 0; i < parts.length; i++) { - if (parts[i].startsWith('rule-') || (parts[i - 1] && parts[i - 1].startsWith('rules'))) { - ruleName = normalize(parts[i]); + if (multirule) { + const ruleName = basename(name); - return packageName ? `${packageName}/${ruleName}` : ruleName; - } + return resolve(dir, `../src/${ruleName}`); } - return ruleName; + return require.resolve(root); }; /**