Skip to content

Commit

Permalink
feat: merge contributor-count axiom into fork
Browse files Browse the repository at this point in the history
  • Loading branch information
prototypicalpro committed Aug 31, 2020
1 parent 002deae commit f3dc857
Show file tree
Hide file tree
Showing 12 changed files with 708 additions and 112 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,31 @@ rules:
...
```

Some axioms (ex. [`contributor-count`](./docs/md/axioms.md#contributor-count)) output numerical values instead of strings. For these axioms, numerical comparisons (`<`, `>`, `<=`, `>=`) can be also be specified in the `where` conditional. Note that if a numerical comparison is used for a non-numerical axiom, the comparison will always fail.
```JavaScript
{
"axioms": {
"contributor-count": "contributors"
},
"rules": {
"my-rule": {
"where": ["contributors>6", "contributors<200"],
// ...
}
}
}
```
```YAML
axioms:
contributor-count: contributors
rules:
my-rule:
where:
- contributors>6
- contributors<200
rule:
...
```
## API

Repolinter also includes an extensible JavaScript API:
Expand Down
3 changes: 2 additions & 1 deletion axioms/axioms.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
module.exports = [
'licensee',
'linguist',
'packagers'
'packagers',
'contributor-count'
]
21 changes: 21 additions & 0 deletions axioms/contributor-count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2017 TODO Group. All rights reserved.
// Licensed under the Apache License, Version 2.0.

const { gitlogPromise } = require('gitlog')
const Result = require('../lib/result')

module.exports = async function (fileSystem) {
const commits = await gitlogPromise({
repo: fileSystem.targetDir,
all: true,
number: 10000 // Fetch the last 10000 commits
})
if (!commits) {
return new Result('GitLog axiom failed to run, is this project a git repository?', [], false)
}
// Get commit authors and filter unique values
const contributors = commits
.map((commit) => commit.authorName.toLowerCase())
.filter((value, index, self) => self.indexOf(value) === index)
return new Result('', [{ path: contributors.length.toString(), passed: true }], true)
}
2 changes: 1 addition & 1 deletion bin/repolinter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const yaml = require('js-yaml')

// eslint-disable-next-line no-unused-expressions
require('yargs')
.command(['lint <directory>', '*'], 'run repolinter on the specified directory, outputting results to STDOUT.', yargs => {
.command(['lint [directory]', '*'], 'run repolinter on the specified directory, outputting results to STDOUT.', yargs => {
yargs
.positional('directory', {
describe: 'The target directory to lint',
Expand Down
29 changes: 25 additions & 4 deletions docs/md/axioms.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,48 @@ Below is a complete list of axioms that Repolinter can check.

- [Contents](#contents)
- [Reference](#reference)
- [`contributor-count`](#contributor-count)
- [`licensee`](#licensee)
- [`linguist`](#linguist)
- [`packagers`](#packagers)

## Reference

### `contributor-count`

This axiom uses [gitlog](https://github.com/domharrington/node-gitlog#readme) to count the number of contributors to the current git repository. Contributors are counted based on unique occurrences of an author name in the git log. This axiom is a numerical axiom, meaning numerical comparisons can be used.

An example of using this axiom:

```JavaScript
{
"axioms": {
"contributor-count": "contributors"
},
"rules": {
"my-rule": {
"where": ["contributors>6", "contributors<200"],
// ...
}
}
}
```

### `licensee`

This axiom uses [licensee](https://github.com/licensee/licensee) to detect the license used in the current repository. To use this axiom licensee must be installed in your `PATH` or in the same directory as Repolinter.
This axiom will return a list of [license identifiers](https://spdx.org/licenses/) associated with the current repository.

An example of using this axiom:
```JSON
```JavaScript
{
"axioms": {
"licensee": "license"
},
"rules": {
"my-rule": {
"where": ["license=Apache-2.0"],
...
// ...
}
}
}
Expand All @@ -37,15 +58,15 @@ An example of using this axiom:
This axiom uses GitHub's [linguist](https://github.com/github/linguist) to detect programming languages in the current repository. To use this axiom, linguist must be installed in the system `PATH` or in the same directory as Repolinter. This axiom will return a lowercase list of programming languages from [this list of supported languages](https://github.com/github/linguist/blob/master/lib/linguist/languages.yml).

An example of using this axiom:
```JSON
```JavaScript
{
"axioms": {
"linguist":" language"
},
"rules": {
"my-rule": {
"where": ["language=javascript"],
...
// ...
}
}
}
Expand Down
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export declare function runRuleset(ruleset: RuleInfo[], targets: boolean|{ [key:
export declare function determineTargets(axiomconfig: any, fs: FileSystem): Promise<{ [key: string]: Result }>
export declare function validateConfig(config: any): Promise<{ passed: boolean, error?: string }>
export declare function parseConfig(config: any): RuleInfo[]
export declare function shouldRuleRun(validTargets: string[], ruleAxioms: string[]): string[]

export declare const defaultFormatter: Formatter
export declare const jsonFormatter: Formatter
Expand Down
68 changes: 66 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,65 @@ async function loadAxioms () {
.reduce((p, [name, require]) => { p[name] = require; return p }, {})
}

/**
* Checks a rule's list of axioms against a list of valid
* targets, and determines if the rule should run or not
* based on the following rules criteria:
* * The rule's list has a direct match on a target OR
* * The rule specifies a numerical axiom (ex. >) and the target
* list contains a target that matches that axiom.
*
* Supported numerical axioms are >, <, >=, <=, and = Only
*
* @memberof repolinter
* @param {string[]} validTargets The axiom target list in "target=thing" format, including the wildcard entry ("target=*").
* For numerical targets it is assumed that only one entry and the wildcard are present (e.g. ["target=2", "target=3", "target=*"] is invalid)
* @param {string[]} ruleAxioms The rule "where" specification to validate against.
* @returns {string[]} The list pf unsatisfied axioms, if any. Empty array indicates the rule should run.
*/
function shouldRuleRun (validTargets, ruleAxioms) {
// parse out numerical axioms, splitting them by name, operand, and number
const ruleRegex = /([\w-]+)((?:>|<)=?)(\d+)/i
const numericalRuleAxioms = []
const regularRuleAxioms = []
for (const ruleax of ruleAxioms) {
const match = ruleRegex.exec(ruleax)
if (match !== null && match[1] && match[2] && !isNaN(parseInt(match[3]))) {
// parse the numerical version
numericalRuleAxioms.push({ axiom: ruleax, name: match[1], operand: match[2], number: parseInt(match[3]) })
} else {
// parse the non-numerical version
regularRuleAxioms.push(ruleax)
}
}
// test that every non-number axiom matches a target
// start a list of condidions that don't pass
const table = new Set(validTargets)
const failedRuleAxioms = regularRuleAxioms.filter(r => !table.has(r))
// check the numbered axioms
// convert the targets into { targetName: number } for all numerical ones
const numericalTargets = validTargets
.map(r => r.split('='))
.map(([name, maybeNumber]) => [name, parseInt(maybeNumber)])
.filter(([name, maybeNumber]) => !isNaN(maybeNumber))
/** @ts-ignore */
const numericalTargetsMap = new Map(numericalTargets)
// test each numerical Rule against it's numerical axiom, return the axioms that failed
return numericalRuleAxioms
.filter(({ axiom, name, operand, number }) => {
// get the number to test against
const target = numericalTargetsMap.get(name)
if (target === undefined) return true
// test the number based on the operand
return !((operand === '<' && target < number) ||
(operand === '<=' && target <= number) ||
(operand === '>' && target > number) ||
(operand === '>=' && target >= number))
})
.map(({ axiom }) => axiom)
.concat(failedRuleAxioms)
}

/**
* Run all operations in a ruleset, including linting and fixing. Returns
* a list of objects with the output of the linter rules
Expand All @@ -282,11 +341,15 @@ async function loadAxioms () {
*/
async function runRuleset (ruleset, targets, fileSystem, dryRun) {
// generate a flat array of axiom string identifiers
/** @ignore @type {string[]} */
let targetArray = []
if (typeof targets !== 'boolean') {
targetArray = Object.entries(targets)
// restricted to only passed axioms
.filter(([axiomId, res]) => res.passed)
// pair the axiom ID with the axiom target array
.map(([axiomId, res]) => [axiomId, res.targets.map(t => t.path)])
// join the target arrays together into one array of all the targets
.map(([axiomId, paths]) => [`${axiomId}=*`].concat(paths.map(p => `${axiomId}=${p}`)))
.reduce((a, c) => a.concat(c), [])
}
Expand All @@ -300,8 +363,8 @@ async function runRuleset (ruleset, targets, fileSystem, dryRun) {
// check axioms and enable appropriately
if (r.level === 'off') { return FormatResult.CreateIgnored(r, 'ignored because level is "off"') }
// filter to only targets with no matches
if (typeof targets !== 'boolean') {
const ignoreReasons = r.where.filter(check => !targetArray.find(tar => check === tar))
if (typeof targets !== 'boolean' && r.where && r.where.length) {
const ignoreReasons = shouldRuleRun(targetArray, r.where)
if (ignoreReasons.length > 0) { return FormatResult.CreateIgnored(r, `ignored due to unsatisfied condition(s): "${ignoreReasons.join('", "')}"`) }
}
// check if the rule file exists
Expand Down Expand Up @@ -444,6 +507,7 @@ module.exports.runRuleset = runRuleset
module.exports.determineTargets = determineTargets
module.exports.validateConfig = validateConfig
module.exports.parseConfig = parseConfig
module.exports.shouldRuleRun = shouldRuleRun
module.exports.lint = lint
module.exports.Result = Result
module.exports.RuleInfo = RuleInfo
Expand Down
Loading

0 comments on commit f3dc857

Please sign in to comment.