Skip to content

Commit

Permalink
New: Use path to load resources in .sonarwhalrc
Browse files Browse the repository at this point in the history
Enable loading a resource using a path instead of the ID of the package.
This is useful when you have multiple configuration files with some
common parts or when you have a locally developed rule that you don't
want to publish on a package.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Fix #1123
  • Loading branch information
molant authored and alrra committed Jul 3, 2018
1 parent 827c5bd commit 1cf4cae
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -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.
88 changes: 88 additions & 0 deletions packages/sonarwhal/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* ------------------------------------------------------------------------------
*/

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

Expand Down Expand Up @@ -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;
}

Expand Down
45 changes: 44 additions & 1 deletion packages/sonarwhal/src/lib/sonarwhal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RuleConfig> => {
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;
Expand All @@ -130,7 +173,7 @@ export class Sonarwhal extends EventEmitter {
ignoredConnectors.includes(connectorId);
};

const ruleOptions: RuleConfig | Array<RuleConfig> = config.rules[id];
const ruleOptions: RuleConfig | Array<RuleConfig> = getRuleConfig(id);
const severity: Severity = getSeverity(ruleOptions);

if (ignoreRule(Rule)) {
Expand Down
39 changes: 28 additions & 11 deletions packages/sonarwhal/src/lib/utils/resource-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/

import * as path from 'path';
import * as fs from 'fs';

import * as globby from 'globby';
import * as semver from 'semver';
Expand Down Expand Up @@ -244,11 +245,13 @@ const generateConfigPathsToResources = (configurations: Array<string>, name: str
*/
export const loadResource = (name: string, type: ResourceType, configurations: Array<string> = [], 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}`;

Expand All @@ -269,21 +272,30 @@ 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<string> = [
`@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<string> = 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;

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) {
Expand Down Expand Up @@ -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;
};
Expand Down
36 changes: 14 additions & 22 deletions packages/sonarwhal/src/lib/utils/rule-helpers.ts
Original file line number Diff line number Diff line change
@@ -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<string>): Array<string> => {
Expand Down Expand Up @@ -26,32 +27,23 @@ export const getIncludedHeaders = (headers: object, headerList: Array<string> =
};

/**
* 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);
};

/**
Expand Down

0 comments on commit 1cf4cae

Please sign in to comment.