Skip to content

Commit

Permalink
refactor: port lint to typescript (#908)
Browse files Browse the repository at this point in the history
* chore(lint): add first draft of lint types

* chore(lint): update package settings for typescript

* fix(is-ignored): explicitly export default from is ignored

* docs(lint): add todo message for plugin types

* refactor: port to ts

* fix: add missing references

* refactor: move shared types to package

* fix: streamline verison

* refactor: reuse RuleSeverity

Co-authored-by: Mario Nebl <marionebl@users.noreply.github.com>
  • Loading branch information
byCedric and marionebl committed Feb 5, 2020
1 parent 830c343 commit 935bc10
Show file tree
Hide file tree
Showing 72 changed files with 300 additions and 151 deletions.
1 change: 1 addition & 0 deletions @commitlint/ensure/package.json
Expand Up @@ -38,6 +38,7 @@
"globby": "11.0.0"
},
"dependencies": {
"@commitlint/types": "^8.3.4",
"lodash": "^4.17.15"
}
}
2 changes: 1 addition & 1 deletion @commitlint/ensure/src/case.ts
Expand Up @@ -3,7 +3,7 @@ import kebabCase from 'lodash/kebabCase';
import snakeCase from 'lodash/snakeCase';
import upperFirst from 'lodash/upperFirst';
import startCase from 'lodash/startCase';
import {TargetCaseType} from '.';
import {TargetCaseType} from '@commitlint/types';

export default ensureCase;

Expand Down
1 change: 0 additions & 1 deletion @commitlint/ensure/src/index.ts
Expand Up @@ -5,7 +5,6 @@ import maxLineLength from './max-line-length';
import minLength from './min-length';
import notEmpty from './not-empty';

export * from './types';
export {ensureCase as case};
export {ensureEnum as enum};
export {maxLength, maxLineLength, minLength, notEmpty};
3 changes: 2 additions & 1 deletion @commitlint/ensure/tsconfig.json
Expand Up @@ -6,5 +6,6 @@
"outDir": "./lib"
},
"include": ["./src/**/*.ts"],
"exclude": ["./src/**/*.test.ts", "./lib/**/*"]
"exclude": ["./src/**/*.test.ts", "./lib/**/*"],
"references": [{ "path": "../types" }]
}
1 change: 1 addition & 0 deletions @commitlint/is-ignored/package.json
Expand Up @@ -40,6 +40,7 @@
"@types/semver": "7.1.0"
},
"dependencies": {
"@commitlint/types": "^8.3.4",
"semver": "7.1.2"
}
}
2 changes: 1 addition & 1 deletion @commitlint/is-ignored/src/defaults.ts
@@ -1,5 +1,5 @@
import * as semver from 'semver';
import {Matcher} from './types';
import {Matcher} from '@commitlint/types';

const isSemver = (c: string): boolean => {
const firstLine = c.split('\n').shift();
Expand Down
2 changes: 1 addition & 1 deletion @commitlint/is-ignored/src/index.ts
@@ -1,2 +1,2 @@
export * from './is-ignored';
export * from './types';
export {default} from './is-ignored';
7 changes: 1 addition & 6 deletions @commitlint/is-ignored/src/is-ignored.ts
@@ -1,10 +1,5 @@
import {wildcards} from './defaults';
import {Matcher} from './types';

export interface IsIgnoredOptions {
ignores?: Matcher[];
defaults?: boolean;
}
import {IsIgnoredOptions} from '@commitlint/types';

export default function isIgnored(
commit: string = '',
Expand Down
1 change: 0 additions & 1 deletion @commitlint/is-ignored/src/types.ts

This file was deleted.

3 changes: 2 additions & 1 deletion @commitlint/is-ignored/tsconfig.json
Expand Up @@ -11,5 +11,6 @@
"exclude": [
"./src/**/*.test.ts",
"./lib/**/*"
]
],
"references": [{"path": "../types"}]
}
14 changes: 4 additions & 10 deletions @commitlint/lint/package.json
Expand Up @@ -2,21 +2,14 @@
"name": "@commitlint/lint",
"version": "8.3.5",
"description": "Lint a string against commitlint rules",
"main": "lib/index.js",
"main": "lib/lint.js",
"types": "lib/lint.d.ts",
"files": [
"lib/"
],
"scripts": {
"build": "cross-env NODE_ENV=production babel src --out-dir lib --source-maps",
"deps": "dep-check",
"pkg": "pkg-check --skip-import",
"start": "yarn run watch",
"watch": "babel src --out-dir lib --watch --source-maps"
},
"babel": {
"presets": [
"babel-preset-commitlint"
]
"pkg": "pkg-check --skip-import"
},
"engines": {
"node": ">=4"
Expand Down Expand Up @@ -53,6 +46,7 @@
"@commitlint/is-ignored": "^8.3.5",
"@commitlint/parse": "^8.3.4",
"@commitlint/rules": "^8.3.4",
"@commitlint/types": "^8.3.4",
"lodash": "^4.17.15"
}
}
18 changes: 18 additions & 0 deletions @commitlint/lint/src/commit-message.ts
@@ -0,0 +1,18 @@
export interface CommitMessageData {
header: string;
body?: string | null;
footer?: string | null;
}

export const buildCommitMesage = ({
header,
body,
footer
}: CommitMessageData): string => {
let message = header;

message = body ? `${message}\n\n${body}` : message;
message = footer ? `${message}\n\n${footer}` : message;

return message;
};
@@ -1,12 +1,12 @@
import lint from '.';
import lint from './lint';

test('throws without params', async () => {
const error = lint();
const error = (lint as any)();
await expect(error).rejects.toThrow('Expected a raw commit');
});

test('throws with empty message', async () => {
const error = lint('');
const error = (lint as any)('');
await expect(error).rejects.toThrow('Expected a raw commit');
});

Expand Down Expand Up @@ -91,7 +91,7 @@ test('throws for invalid rule config', async () => {
const error = lint('type(scope): foo', {
'type-enum': 1,
'scope-enum': {0: 2, 1: 'never', 2: ['foo'], length: 3}
});
} as any);

await expect(error).rejects.toThrow('type-enum must be array');
await expect(error).rejects.toThrow('scope-enum must be array');
Expand All @@ -109,15 +109,15 @@ test('allows disable shorthand', async () => {
});

test('throws for rule with invalid length', async () => {
const error = lint('type(scope): foo', {'scope-enum': [1, 2, 3, 4]});
const error = lint('type(scope): foo', {'scope-enum': [1, 2, 3, 4]} as any);

await expect(error).rejects.toThrow('scope-enum must be 2 or 3 items long');
});

test('throws for rule with invalid level', async () => {
const error = lint('type(scope): foo', {
'type-enum': ['2', 'always'],
'header-max-length': [{}, 'always']
'type-enum': ['2', 'always'] as any,
'header-max-length': [{}, 'always'] as any
});
await expect(error).rejects.toThrow('rule type-enum must be number');
await expect(error).rejects.toThrow('rule header-max-length must be number');
Expand All @@ -137,8 +137,8 @@ test('throws for rule with out of range level', async () => {

test('throws for rule with invalid condition', async () => {
const error = lint('type(scope): foo', {
'type-enum': [1, 2],
'header-max-length': [1, {}]
'type-enum': [1, 2] as any,
'header-max-length': [1, {}] as any
});

await expect(error).rejects.toThrow('type-enum must be string');
Expand All @@ -147,8 +147,8 @@ test('throws for rule with invalid condition', async () => {

test('throws for rule with out of range condition', async () => {
const error = lint('type(scope): foo', {
'type-enum': [1, 'foo'],
'header-max-length': [1, 'bar']
'type-enum': [1, 'foo'] as any,
'header-max-length': [1, 'bar'] as any
});

await expect(error).rejects.toThrow('type-enum must be "always" or "never"');
Expand Down
85 changes: 50 additions & 35 deletions @commitlint/lint/src/index.js → @commitlint/lint/src/lint.ts
@@ -1,20 +1,29 @@
import util from 'util';
import isIgnored from '@commitlint/is-ignored';
import parse from '@commitlint/parse';
import implementations from '@commitlint/rules';
import defaultRules from '@commitlint/rules';
import toPairs from 'lodash/toPairs';
import values from 'lodash/values';
import {buildCommitMesage} from './commit-message';
import {
LintRuleConfig,
LintOptions,
LintRuleOutcome,
Rule,
Plugin,
RuleSeverity
} from '@commitlint/types';

export default async function lint(
message: string,
rawRulesConfig?: LintRuleConfig,
rawOpts?: LintOptions
) {
const opts = rawOpts
? rawOpts
: {defaultIgnores: undefined, ignores: undefined};
const rulesConfig = rawRulesConfig || {};

const buildCommitMesage = ({header, body, footer}) => {
let message = header;

message = body ? `${message}\n\n${body}` : message;
message = footer ? `${message}\n\n${footer}` : message;

return message;
};

export default async (message, rules = {}, opts = {}) => {
// Found a wildcard match, skip
if (
isIgnored(message, {defaults: opts.defaultIgnores, ignores: opts.ignores})
Expand All @@ -29,33 +38,35 @@ export default async (message, rules = {}, opts = {}) => {

// Parse the commit message
const parsed = await parse(message, undefined, opts.parserOpts);
const allRules: Map<string, Rule<unknown> | Rule<never>> = new Map(
Object.entries(defaultRules)
);

const mergedImplementations = Object.assign({}, implementations);
if (opts.plugins) {
values(opts.plugins).forEach(plugin => {
values(opts.plugins).forEach((plugin: Plugin) => {
if (plugin.rules) {
Object.keys(plugin.rules).forEach(ruleKey => {
mergedImplementations[ruleKey] = plugin.rules[ruleKey];
});
Object.keys(plugin.rules).forEach(ruleKey =>
allRules.set(ruleKey, plugin.rules[ruleKey])
);
}
});
}

// Find invalid rules configs
const missing = Object.keys(rules).filter(
name => typeof mergedImplementations[name] !== 'function'
const missing = Object.keys(rulesConfig).filter(
name => typeof allRules.get(name) !== 'function'
);

if (missing.length > 0) {
const names = Object.keys(mergedImplementations);
const names = [...allRules.keys()];
throw new RangeError(
`Found invalid rule names: ${missing.join(
', '
)}. Supported rule names are: ${names.join(', ')}`
);
}

const invalid = toPairs(rules)
const invalid = toPairs(rulesConfig)
.map(([name, config]) => {
if (!Array.isArray(config)) {
return new Error(
Expand All @@ -65,7 +76,13 @@ export default async (message, rules = {}, opts = {}) => {
);
}

const [level, when] = config;
const [level] = config;

if (level === RuleSeverity.Disabled && config.length === 1) {
return null;
}

const [, when] = config;

if (typeof level !== 'number' || isNaN(level)) {
return new Error(
Expand All @@ -75,10 +92,6 @@ export default async (message, rules = {}, opts = {}) => {
);
}

if (level === 0 && config.length === 1) {
return null;
}

if (config.length !== 2 && config.length !== 3) {
return new Error(
`config for rule ${name} must be 2 or 3 items long, received ${util.inspect(
Expand Down Expand Up @@ -113,18 +126,15 @@ export default async (message, rules = {}, opts = {}) => {

return null;
})
.filter(item => item instanceof Error);
.filter((item): item is Error => item instanceof Error);

if (invalid.length > 0) {
throw new Error(invalid.map(i => i.message).join('\n'));
}

// Validate against all rules
const results = toPairs(rules)
.filter(entry => {
const [, [level]] = toPairs(entry);
return level > 0;
})
const results = toPairs(rulesConfig)
.filter(([, [level]]) => level > 0)
.map(entry => {
const [name, config] = entry;
const [level, when, value] = config;
Expand All @@ -134,9 +144,14 @@ export default async (message, rules = {}, opts = {}) => {
return null;
}

const rule = mergedImplementations[name];
const rule = allRules.get(name);

if (!rule) {
throw new Error(`Could not find rule implementation for ${name}`);
}

const [valid, message] = rule(parsed, when, value);
const executableRule = rule as Rule<unknown>;
const [valid, message] = executableRule(parsed, when, value);

return {
level,
Expand All @@ -145,7 +160,7 @@ export default async (message, rules = {}, opts = {}) => {
message
};
})
.filter(Boolean);
.filter((result): result is LintRuleOutcome => result !== null);

const errors = results.filter(result => result.level === 2 && !result.valid);
const warnings = results.filter(
Expand All @@ -160,4 +175,4 @@ export default async (message, rules = {}, opts = {}) => {
warnings,
input: buildCommitMesage(parsed)
};
};
}
21 changes: 21 additions & 0 deletions @commitlint/lint/tsconfig.json
@@ -0,0 +1,21 @@
{
"extends": "../../tsconfig.shared.json",
"compilerOptions": {
"composite": true,
"rootDir": "./src",
"outDir": "./lib"
},
"include": [
"./src"
],
"exclude": [
"./src/**/*.test.ts",
"./lib/**/*"
],
"references": [
{"path": "../is-ignored"},
{"path": "../parse"},
{"path": "../rules"},
{"path": "../types"}
]
}
1 change: 1 addition & 0 deletions @commitlint/load/package.json
Expand Up @@ -41,6 +41,7 @@
"dependencies": {
"@commitlint/execute-rule": "^8.3.4",
"@commitlint/resolve-extends": "^8.3.5",
"@commitlint/types": "^8.3.5",
"chalk": "3.0.0",
"cosmiconfig": "^6.0.0",
"lodash": "^4.17.15",
Expand Down

0 comments on commit 935bc10

Please sign in to comment.