Skip to content

Commit 6c3bff2

Browse files
committed
Adding in subject rules and tests.
1 parent 0a8a802 commit 6c3bff2

12 files changed

+794
-0
lines changed

packages/commitlint-plugin-github-rules/src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,29 @@ import githubIssueNumberMissingRuleResolver from './rules/githubIssueNumbers/isM
55
import githubIssueNumberFormatRuleResolver from './rules/githubIssueNumbers/isCorrectFormat';
66
import githubIssueNumberDuplicateRuleResolver from './rules/githubIssueNumbers/isDuplicate';
77

8+
import {
9+
subjectEmptyRuleResolver,
10+
subjectCaseRuleResolver,
11+
subjectFullStopRuleResolver,
12+
subjectMinLengthRuleResolver,
13+
subjectMaxLengthRuleResolver,
14+
subjectSeparatorRuleResolver,
15+
} from './rules/subject';
16+
817
const commitlintGitHubRules = utils.commitlintGitHubConstants.GITHUB_RULES;
918

1019
export const commitlintPluginGitHub: CommitlintPluginGitHub = {
1120
rules: {
1221
[commitlintGitHubRules.issueNumberMissing]: githubIssueNumberMissingRuleResolver,
1322
[commitlintGitHubRules.issueNumberFormat]: githubIssueNumberFormatRuleResolver,
1423
[commitlintGitHubRules.issueNumberDuplicate]: githubIssueNumberDuplicateRuleResolver,
24+
25+
[commitlintGitHubRules.subjectEmpty]: subjectEmptyRuleResolver,
26+
[commitlintGitHubRules.subjectCase]: subjectCaseRuleResolver,
27+
[commitlintGitHubRules.subjectFullStop]: subjectFullStopRuleResolver,
28+
[commitlintGitHubRules.subjectMinLength]: subjectMinLengthRuleResolver,
29+
[commitlintGitHubRules.subjectMinLength]: subjectMaxLengthRuleResolver,
30+
[commitlintGitHubRules.subjectSeparator]: subjectSeparatorRuleResolver,
1531
},
1632
};
1733

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ParsedCommitMessage } from 'commitlint-github-utils';
2+
3+
import { BaseParsedCommit, RuleResolverResult, RuleResolver } from '../../../@types';
4+
import resolveRuleUsingBaseResolver from '../utils/wrappedRuleResolver';
5+
6+
// TODO Get rewire to work or some other solution to avoid exporting subjectAdapter and subjectRuleResolver for testing
7+
8+
export function subjectAdapter(parsed: ParsedCommitMessage): BaseParsedCommit {
9+
return { subject: parsed.subject };
10+
}
11+
12+
export function subjectRuleResolver<T>(baseRuleResolver: RuleResolver<T>, defaultValue?: T): RuleResolver<T> {
13+
return (parsed, when, valuePassedIn): RuleResolverResult => {
14+
let value = valuePassedIn;
15+
if (value == null) {
16+
value = defaultValue;
17+
}
18+
19+
return resolveRuleUsingBaseResolver(baseRuleResolver, subjectAdapter, parsed, when, value);
20+
};
21+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// FIXME: Temporarily disabling TypeScript checking on importing base rules
2+
// until @commitlint/rules exports index.d.ts which should be soon:
3+
// See: https://github.com/conventional-changelog/commitlint/issues/659
4+
5+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
6+
// @ts-ignore
7+
import baseRules from '@commitlint/rules';
8+
// import { TargetCaseType } from '@commitlint/types'; // When it is published
9+
import { TargetCaseType } from 'commitlint-github-utils/@types';
10+
import utils from 'commitlint-github-utils';
11+
12+
import { RuleResolver } from '../../../@types';
13+
import { subjectRuleResolver } from './helpers';
14+
15+
import subjectSeparatorRuleResolver from './subjectSeparator';
16+
17+
const commitlintGitHubRules = utils.commitlintGitHubConstants.GITHUB_RULES;
18+
19+
// Delegate Subject Rules
20+
21+
export const subjectEmptyRuleResolver: RuleResolver<unknown> = subjectRuleResolver(
22+
baseRules[commitlintGitHubRules.subjectEmpty],
23+
);
24+
25+
export const subjectCaseRuleResolver: RuleResolver<TargetCaseType | TargetCaseType[]> = subjectRuleResolver(
26+
baseRules[commitlintGitHubRules.subjectCase],
27+
);
28+
29+
export const subjectFullStopRuleResolver: RuleResolver<string> = subjectRuleResolver(
30+
baseRules[commitlintGitHubRules.subjectFullStop],
31+
'.',
32+
);
33+
34+
export const subjectMinLengthRuleResolver: RuleResolver<number> = subjectRuleResolver(
35+
baseRules[commitlintGitHubRules.subjectMinLength],
36+
);
37+
38+
export const subjectMaxLengthRuleResolver: RuleResolver<number> = subjectRuleResolver(
39+
baseRules[commitlintGitHubRules.subjectMaxLength],
40+
);
41+
42+
// Custom Subject Rules
43+
44+
export { subjectSeparatorRuleResolver };
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { When, ParsedCommitMessage } from 'commitlint-github-utils/@types';
2+
import utils from 'commitlint-github-utils';
3+
4+
import { RuleResolverResult, RuleResolver } from '../../../@types';
5+
6+
// For now this is hardcoded because to support custom separators the parser in commitlint-github-utils
7+
// would need to be updated as use static regex patterns to parse commit messages
8+
const EXPECTED_SEPARATOR = ' ';
9+
10+
const subjectSeparatorRuleResolver: RuleResolver<string> = (parsed, whenPassedIn): RuleResolverResult => {
11+
const rawCommitMessage = parsed.raw;
12+
if (!rawCommitMessage) return [false, 'Commit message should not be empty'];
13+
14+
const commitMessage = utils.parseCommitMessage(rawCommitMessage);
15+
16+
// We short circuit if the When requested is only for non-WIPs and the commit is a WIP
17+
const wipHandledResult = utils.handleWipCommits(commitMessage, whenPassedIn);
18+
if (wipHandledResult.isWipValidated) {
19+
// In that case we just return true immediately
20+
return [true];
21+
}
22+
// Otherwise we continue with validating the separator with the When returned
23+
// as if it was passed a NON-WIPs one it has been converted to a standard ALWAYS or NEVER
24+
// so validateSubjectSeparator() only needs to handle standard When values. See the docs on handleWipCommits() for more info
25+
26+
return validateSubjectSeparator(commitMessage, wipHandledResult.when);
27+
};
28+
29+
function validateSubjectSeparator(commitMessage: ParsedCommitMessage, when: When = When.ALWAYS): RuleResolverResult {
30+
const { subjectSeparator, subject } = commitMessage;
31+
32+
// If there is no subject defined (not an empty subject) then we have nothing to validate so we return true
33+
if (subject === undefined) {
34+
return [true];
35+
}
36+
37+
// Negated instance doesn't make much sense but if negated then we validate there is NO separator of a particular type before the subject
38+
if (utils.isNegated(when)) {
39+
return [
40+
subjectSeparator !== EXPECTED_SEPARATOR,
41+
`the commit message has a separator '${EXPECTED_SEPARATOR}' before the subject which isn't allowed`,
42+
];
43+
}
44+
45+
// But by default we validate that there IS a separator of a particular type before the subject
46+
return [
47+
subjectSeparator === EXPECTED_SEPARATOR,
48+
`the commit message does not have a valid separator before the subject; expected: '${EXPECTED_SEPARATOR}'`,
49+
];
50+
}
51+
52+
export default subjectSeparatorRuleResolver;
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// See comment in subjectRuleResolvers.ts for why these checks are disabled:
2+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
3+
// @ts-ignore
4+
import baseRules from '@commitlint/rules';
5+
import { When } from 'commitlint-github-utils/@types';
6+
import utils from 'commitlint-github-utils';
7+
8+
import { BaseParsedCommit, RuleResolverResult, RuleResolver } from '../../../../@types';
9+
import { subjectAdapter } from '../helpers';
10+
import * as SubjectRuleResolvers from '..';
11+
12+
import resolveRuleUsingBaseResolver from '../../utils/wrappedRuleResolver';
13+
14+
// Mock the resolveRuleUsingBaseResolver so we can isolate our test to just the function-under-test: subjectRuleResolver
15+
jest.mock('../../utils/wrappedRuleResolver');
16+
17+
const mockedResolveRuleUsingBaseResolver: jest.Mock<RuleResolverResult> = resolveRuleUsingBaseResolver as jest.Mock<
18+
RuleResolverResult
19+
>;
20+
21+
const commitlintGitHubRules = utils.commitlintGitHubConstants.GITHUB_RULES;
22+
23+
const EXPECTED_PARSED: BaseParsedCommit = { raw: 'dummy-raw' };
24+
const EXPECTED_WHEN = When.NEVER;
25+
const EXPECTED_VALUE = 'dummy-value';
26+
27+
function validateDelegateRuleResolver(
28+
delegateRuleName: string,
29+
subjectRuleResolver: RuleResolver<any>,
30+
defaultValue?: string,
31+
): void {
32+
// Given that the standard subject rule exists
33+
const delegate = baseRules[delegateRuleName];
34+
expect(delegate).toBeDefined();
35+
36+
// And that the wrapped subject rule under test exists
37+
expect(subjectRuleResolver).toBeDefined();
38+
39+
// And we mock the base rule resolving function to return a mock response
40+
const mockResult: RuleResolverResult = [true, delegateRuleName];
41+
mockedResolveRuleUsingBaseResolver.mockClear();
42+
mockedResolveRuleUsingBaseResolver.mockReturnValue(mockResult);
43+
44+
// Unless a default value was passed into this test, we will pass the placeholder EXPECTED_VALUE
45+
// and expect that to be passed to the base rule resolving function
46+
let valuePassedIn: string | null = EXPECTED_VALUE;
47+
let expectedValue = valuePassedIn;
48+
49+
// Otherwise, if a default value is specified we'll won't pass a value in but expect the default value to be used instead
50+
if (defaultValue) {
51+
valuePassedIn = null;
52+
expectedValue = defaultValue;
53+
}
54+
55+
// When we call the subject rule under test
56+
const result = subjectRuleResolver(EXPECTED_PARSED, EXPECTED_WHEN, valuePassedIn);
57+
58+
// Then we expect the mock response from the base rule resolving function to be returned
59+
expect(result).toEqual(mockResult);
60+
61+
// And we expect the base rule resolving function with the correct arguments
62+
expect(mockedResolveRuleUsingBaseResolver).toHaveBeenCalledWith(
63+
delegate,
64+
subjectAdapter,
65+
EXPECTED_PARSED,
66+
EXPECTED_WHEN,
67+
expectedValue,
68+
);
69+
70+
// The base rule resolving function itself is tested by wrappedRuleResolver.test.ts
71+
// We also have black box tests in this directory
72+
}
73+
74+
describe('delegate subject rule resolvers', () => {
75+
it('subjectEmptyRuleResolver should delegate correctly for non-WIP commits only', () => {
76+
validateDelegateRuleResolver(commitlintGitHubRules.subjectEmpty, SubjectRuleResolvers.subjectEmptyRuleResolver);
77+
});
78+
79+
it('subjectCaseRuleResolver should delegate correctly for non-WIP commits only', () => {
80+
validateDelegateRuleResolver(commitlintGitHubRules.subjectCase, SubjectRuleResolvers.subjectCaseRuleResolver);
81+
});
82+
83+
it('subjectFullStopRuleResolver should delegate correctly for non-WIP commits only, using explicit value if passed', () => {
84+
validateDelegateRuleResolver(
85+
commitlintGitHubRules.subjectFullStop,
86+
SubjectRuleResolvers.subjectFullStopRuleResolver,
87+
);
88+
});
89+
90+
it('subjectFullStopRuleResolver should delegate correctly for non-WIP commits only, defaulting to default value if no value passed', () => {
91+
validateDelegateRuleResolver(
92+
commitlintGitHubRules.subjectFullStop,
93+
SubjectRuleResolvers.subjectFullStopRuleResolver,
94+
// Verify default value is used when no explict value passed
95+
'.',
96+
);
97+
});
98+
99+
it('subjectMinLengthRuleResolver should delegate correctly for non-WIP commits only', () => {
100+
validateDelegateRuleResolver(
101+
commitlintGitHubRules.subjectMinLength,
102+
SubjectRuleResolvers.subjectMinLengthRuleResolver,
103+
);
104+
});
105+
106+
it('subjectMaxLengthRuleResolver should delegate correctly for non-WIP commits only', () => {
107+
validateDelegateRuleResolver(
108+
commitlintGitHubRules.subjectMaxLength,
109+
SubjectRuleResolvers.subjectMaxLengthRuleResolver,
110+
);
111+
});
112+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { ParsedCommitMessage, When } from 'commitlint-github-utils/@types';
2+
import { RuleResolver, RuleResolverResult, BaseParsedCommit } from '../../../../@types';
3+
import { subjectAdapter, subjectRuleResolver } from '../helpers';
4+
5+
import resolveRuleUsingBaseResolver from '../../utils/wrappedRuleResolver';
6+
7+
// Mock the resolveRuleUsingBaseResolver so we can isolate our test to just the function-under-test: subjectRuleResolver
8+
jest.mock('../../utils/wrappedRuleResolver');
9+
10+
const mockedResolveRuleUsingBaseResolver: jest.Mock<RuleResolverResult> = resolveRuleUsingBaseResolver as jest.Mock<
11+
RuleResolverResult
12+
>;
13+
14+
const EXPECTED_PARSED: BaseParsedCommit = { raw: 'dummy-raw' };
15+
const EXPECTED_WHEN = When.NEVER;
16+
const EXPECTED_VALUE = 'dummy-value';
17+
18+
const BASE_RESOLVER: RuleResolver<string> = jest.fn();
19+
const RESULT_FROM_BASE_RESOLVER: RuleResolverResult = [true, 'DUMMY_RESULT'];
20+
21+
mockedResolveRuleUsingBaseResolver.mockReturnValue(RESULT_FROM_BASE_RESOLVER);
22+
23+
describe('subjectAdapter', () => {
24+
it('should return the subject from the ParsedCommitMessage', () => {
25+
const subject = 'expected-subject';
26+
const commit: ParsedCommitMessage = {
27+
issueNumbers: [],
28+
isWip: false,
29+
body: [],
30+
subject,
31+
};
32+
33+
expect(subjectAdapter(commit)).toEqual({ subject });
34+
});
35+
});
36+
37+
describe('subjectRuleResolver', () => {
38+
const defaultValue = 'default-value' as string;
39+
40+
const ruleResolverWithDefaultValue: RuleResolver<string> = subjectRuleResolver(BASE_RESOLVER, defaultValue);
41+
42+
it('should use the arguments passed, including value in when specified', () => {
43+
expect(ruleResolverWithDefaultValue(EXPECTED_PARSED, EXPECTED_WHEN, EXPECTED_VALUE)).toEqual(
44+
RESULT_FROM_BASE_RESOLVER,
45+
);
46+
47+
expect(mockedResolveRuleUsingBaseResolver).toHaveBeenCalledWith(
48+
BASE_RESOLVER,
49+
subjectAdapter,
50+
EXPECTED_PARSED,
51+
EXPECTED_WHEN,
52+
EXPECTED_VALUE,
53+
);
54+
});
55+
56+
it('should use the arguments passed with default value being used when value not specified', () => {
57+
expect(ruleResolverWithDefaultValue(EXPECTED_PARSED, EXPECTED_WHEN, undefined)).toEqual(RESULT_FROM_BASE_RESOLVER);
58+
59+
expect(mockedResolveRuleUsingBaseResolver).toHaveBeenCalledWith(
60+
BASE_RESOLVER,
61+
subjectAdapter,
62+
EXPECTED_PARSED,
63+
EXPECTED_WHEN,
64+
defaultValue,
65+
);
66+
});
67+
});

0 commit comments

Comments
 (0)