Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/jest/setup/console.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
global.console.log = jest.fn();
global.console.info = jest.fn();
global.console.warn = jest.fn();
global.console.error = jest.fn();
7 changes: 4 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ module.exports = {
'!src/core/index.ts',
'!cli.ts'
],
setupFiles: [
'<rootDir>/config/jest/setup/console.setup.ts'
],
coveragePathIgnorePatterns: [
'.*\\.d\\.ts'
],
Expand All @@ -28,9 +31,7 @@ module.exports = {
'node'
],
globals: {
'ts-jest': {
tsConfig: 'tsconfig.spec.json'
}
'ts-jest': { tsConfig: 'tsconfig.spec.json' }
},
collectCoverage: true,
coverageThreshold: {
Expand Down
77 changes: 29 additions & 48 deletions src/core/core.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,123 +20,104 @@ describe('Core', () => {

beforeEach(() => {
process = prepareConfig(process) as Process;
console.error = jest.fn();
console.info = jest.fn();
});

it('should be failed with errored body, empty prefix and a lack of module', () => {
process = commitWith('some error message', process);
validate();
expect(getFirstExitStatus(process)).toBe(1);
expect(() => validate()).toThrowError();
});

it('should be failed if there is a lack of issue prefix', () => {
process = commitWith('common: changed structure of project', process);
validate();
expect(getFirstExitStatus(process)).toBe(1);
expect(() => validate()).toThrowError();
});

it('should be failed with present tense verb', () => {
process = commitWith('#12 common: fix problem', process);
validate();
expect(getFirstExitStatus(process)).toBe(1);
expect(() => validate()).toThrowError();
});

it('should be passed with correct message in nested module', () => {
process = commitWith('#12 actions/panel: fixed openening cards', process);
validate();
expect(getFirstExitStatus(process)).toBe(0);
process = commitWith('#12 actions/panel: fixed opening cards', process);
expect(() => validate()).not.toThrowError();
});

it('should be failed if issue prefix is not in file', () => {
process = commitWith('COREUI-20 actions/panel: fixed openening cards', process);
validate();
expect(getFirstExitStatus(process)).toBe(1);
process = commitWith('COREUI-20 actions/panel: fixed opening cards', process);
expect(() => validate()).toThrowError();
});

it('should be passed if issue prefix is in file', () => {
process = commitWith('TAX-228 actions/panel: fixed openening cards', process);
validate();
expect(getFirstExitStatus(process)).toBe(0);
process = commitWith('TAX-228 actions/panel: fixed opening cards', process);
expect(() => validate()).not.toThrowError();
});

it('should be passed if alternative issue prefix is in file', () => {
process = commitWith('proposal/commit-linter common: added library', process);
validate();
expect(getFirstExitStatus(process)).toBe(0);
expect(() => validate()).not.toThrowError();
});

it('should be passed if issue prefix is in file', () => {
process = commitWith('TAX-228 actions/panel: fixed openening cards', process);
validate();
expect(getFirstExitStatus(process)).toBe(0);
process = commitWith('TAX-228 actions/panel: fixed opening cards', process);
expect(() => validate()).not.toThrowError();
});

it('should be failed if issue prefix is written in different case', () => {
process = commitWith('tax-228 actions/panel: fixed opening cards', process);
expect(() => validate()).toThrowError();
});

it('should be failed if action starts with capital letter', () => {
process = commitWith('TAX-228 actions/panel: Fixed openening cards', process);
validate();
expect(getFirstExitStatus(process)).toBe(1);
process = commitWith('TAX-228 actions/panel: Fixed opening cards', process);
expect(() => validate()).toThrowError();
});

it('should be passed if commit message includes big letters and points in the middle', () => {
process = commitWith('#12 common: added rule about proposals to README.md', process);
validate();
expect(getFirstExitStatus(process)).toBe(0);
expect(() => validate()).not.toThrowError();
});

it('should be failed if there is a point in the end of commit message', () => {
process = commitWith('#12 common: removed all errors.', process);
validate();
expect(getFirstExitStatus(process)).toBe(1);
expect(() => validate()).toThrowError();
});

it('should be passed with correct message in multiple modules', () => {
process = commitWith('#12 actions: fixed openening cards; common/icons: added close icon', process);
validate();
expect(getFirstExitStatus(process)).toBe(0);
process = commitWith('#12 actions: fixed opening cards; common/icons: added close icon', process);
expect(() => validate()).not.toThrowError();
});

it('should be passed with ignore pattern in merge', () => {
process = commitWith('Merge branch \'dev\'', process);
validate();
expect(getFirstExitStatus(process)).toBe(0);
expect(() => validate()).not.toThrowError();
});

it('should be passed with ignore pattern in auto', () => {
process = commitWith('auto/ci: set version 1.2.5', process);
validate();
expect(getFirstExitStatus(process)).toBe(0);
expect(() => validate()).not.toThrowError();
});

it('should be failed if there are no body', () => {
process = commitWith('#12 actions', process);
validate();
expect(getFirstExitStatus(process)).toBe(1);
expect(() => validate()).toThrowError();
});

it('should be passed with microfix issue prefix', () => {
process = commitWith('microfix common: fixed dependency number', process);
validate();
expect(getFirstExitStatus(process)).toBe(0);
expect(() => validate()).not.toThrowError();
});

it('should be passed for config without prefixes', () => {
process = prepareConfig(
process,
'./src/core/spec-assets/spec.config.without-prefix.json'
) as Process;
process = commitWith('cards: fixed padding in title', process);

validate();
process = commitWith('cards: fixed padding in title', process);

expect(getFirstExitStatus(process)).toBe(0);
expect(() => validate()).not.toThrowError();
});

function getFirstExitStatus(process: Process): number {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (process.exit as any as Mock<number>).mock.calls[0][0];
}

function prepareConfig(process: Process, customConfigPath?: string): Process | { exit: Mock<number> } {
const config = customConfigPath || './src/core/spec-assets/spec.config.json';

Expand Down
50 changes: 20 additions & 30 deletions src/core/core.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,32 @@
import { Config } from '../types';
import { TokenDictionary } from './core.interface';
import { exitWithSuccess, getCommitMessage, getConfig } from '../lib';
import { StatusMessage } from '../common/const';
import { checkForBodies, checkForIgnoreMatching, checkForIssuePrefix } from '../linter';
import { getCommitMessage, getConfig } from '../lib';
import { checkForBodies, checkForIssuePrefix, isMessageShouldBeIgnored } from '../linter';
import { getTokensFrom } from '../token-extractor';

/**
* A main function for commit linting. If there are no any errors exit process with 0, else with 1.
* A main function for commit linting.
*/
export function validate(): void {
const config = getConfig();
const commitMessage = getCommitMessage();
const tokens = getTokensFrom(commitMessage, config);

checkForIgnoreMatching(config, tokens);
checkForIssuePrefix(config, tokens);
checkForBodies(config, tokens);
const isIgnored = isMessageShouldBeIgnored(config, tokens);

exitWithSuccess(StatusMessage.VALID);
}

/**
* Parse tokens from commit message.
* | COREUI-220123 common: added the ability to parse library; card: added user |
* | <whole string > |
* | <issue prefix> <bodies > |
* | <body 1 ><body 2 > |
*
* @param message
* @param config
*/
function getTokensFrom(message: string, config: Config): TokenDictionary {
const wholeString = message;
if (isIgnored) {
return;
}

const hasIssuePrefixes = config.issuePrefixes && !config.issuePrefixes.includes('.*');
const [issuePrefix, ...rest] = hasIssuePrefixes ? message.split(' ') : ['', message];
const bodies = rest.join(' ')
.split(';')
.map(body => body.trim());
const checkers = [checkForIssuePrefix, checkForBodies];

return { wholeString, issuePrefix, bodies };
checkers.forEach(checker => {
const error = checker(config, tokens);
if (error) {
throw new Error(error);
}
});
const error = checkers[i](config, tokens);
if (error) {
throw new Error(error);
}
}
}
25 changes: 1 addition & 24 deletions src/lib/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,6 @@ import { ArgumentParser } from '@eigenspace/argument-parser';
import fs from 'fs';
import { Config, Dictionary } from '../types';

/**
* Exit from process with success and print info message in console
*
* @param message
*/
function exitWithSuccess(message: string): void {
console.info(message);
process.exit(0);
}

/**
* Exit from process with error message and print error message in console
*
* @param message
*/
function exitWithError(message: string): void {
console.error(message);
process.exit(1);
}

const DEFAULT_COMMIT_CONFIG_PATH = '.commit-linter.config.json';

/**
Expand Down Expand Up @@ -80,8 +60,7 @@ function getConfigContentFrom(path?: string): Config | undefined {
const content = fs.readFileSync(path, 'utf8');
config = JSON.parse(content);
} catch (err) {
exitWithError(`Failed to load ${path}`);
return;
throw new Error(`Failed to load ${path}`);
}

return config;
Expand Down Expand Up @@ -121,8 +100,6 @@ function merge<T>(...objects: any[]): T {
}

export {
exitWithError,
exitWithSuccess,
getCommitMessage,
getConfigContentFrom,
merge
Expand Down
22 changes: 22 additions & 0 deletions src/linter/linter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { checkForIssuePrefix } from './linter';

describe('Linter', () => {

describe('#checkForIssuePrefix', () => {

it('should pass validating prefix', () => {
const config = { issuePrefixes: ['SC-[0-9]+'] };
const result = checkForIssuePrefix(config, { issuePrefix: 'SC-123' });
expect(result).toBeNull();
});

it('should return error for a prefix in another case', () => {
const config = { issuePrefixes: ['SC-[0-9]+'] };

const result = checkForIssuePrefix(config, { issuePrefix: 'sc-123' });

expect(result).toBeDefined();
expect(result).not.toBeNull();
});
});
});
39 changes: 17 additions & 22 deletions src/linter/linter.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,50 @@
import { Config } from '../types';
import { TokenDictionary } from '../core/core.interface';
import { exitWithError, exitWithSuccess } from '../lib';
import { ErrorMessage, StatusMessage, StringValue } from '../common/const';
import { ErrorMessage, StringValue } from '../common/const';

type ValidationError = string;
type ValidationResult = ValidationError | null;

/**
* Exit from process with code 0 if commit message matches with some of ignore patterns.
* Return true if message should be ignored.
*
* For instance, if there is a ignore pattern '^Merge .*' commit with message "Merge to dev" will
* be accepted.
*
* @param config
* @param wholeString
*/
export function checkForIgnoreMatching(config: Config, { wholeString = '' }: TokenDictionary): void {
if ((config.ignore || []).some((rule: string) => wholeString.match(rule))) {
exitWithSuccess(StatusMessage.VALID);
}
export function isMessageShouldBeIgnored(config: Config, { wholeString = '' }: TokenDictionary): boolean {
return (config.ignore || []).some((rule: string) => wholeString.match(rule));
}

/**
* Exit from process with error code if issue prefix doesn't match any of patterns.
* Return error message if checking was failed.
*
* @param config
* @param issuePrefix
*/
export function checkForIssuePrefix(config: Config, { issuePrefix = '' }: TokenDictionary): void {
export function checkForIssuePrefix(config: Config, { issuePrefix = '' }: TokenDictionary): ValidationResult {
if ((config.issuePrefixes || []).some(value => issuePrefix.match(value))) {
return;
return null;
}

exitWithError(
ErrorMessage.ISSUE_PREFIX_ERROR.replace(StringValue.ISSUE_PREFIX, issuePrefix)
.replace(StringValue.DOC_LINK, config.linkToRule as string)
);
return ErrorMessage.ISSUE_PREFIX_ERROR.replace(StringValue.ISSUE_PREFIX, issuePrefix)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have doubts about it. The more expected behavior is returning true or false. True in success cases. In a more classic way (as I observed), the function returns [result, error]. But these functions are for network interaction.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are not right if you speak that returning boolean is more classic. Do you have any proofs? Otherwise, you can see any validation lib.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway it depends on your wishes. If you wanna return some specific validation error, it's a good solution. Otherwise, if you are enough boolean, you can change it to boolean.

.replace(StringValue.DOC_LINK, config.linkToRule as string);
}

/**
* Exit from process with error code if at least one of bodies doesn't match rule.
* Also prints in console list of not matched bodies.
* Return error message if checking was failed.
*
* @param config
* @param bodies
*/
export function checkForBodies(config: Config, { bodies = [] }: TokenDictionary): void {
export function checkForBodies(config: Config, { bodies = [] }: TokenDictionary): ValidationResult {
if (bodies.every(body => body.match(config.body as RegExp))) {
return;
return null;
}

const notMatchedBodies = bodies.filter(body => !body.match(config.body as RegExp));
exitWithError(
ErrorMessage.BODIES_ERROR.replace(StringValue.BODIES, notMatchedBodies.join('", "'))
.replace(StringValue.DOC_LINK, config.linkToRule as string)
);
return ErrorMessage.BODIES_ERROR.replace(StringValue.BODIES, notMatchedBodies.join('", "'))
.replace(StringValue.DOC_LINK, config.linkToRule as string);
}
1 change: 1 addition & 0 deletions src/token-extractor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './token-extractor';
Loading