From 8b394c94ffe37322d734bd4944add4a6cb2a4689 Mon Sep 17 00:00:00 2001 From: Luis Silva Date: Tue, 18 Feb 2020 09:08:19 +0000 Subject: [PATCH] feat(config-conventional): footer/body-max-line (#436) * feat(config-conventional): footer/body-max-line Co-authored-by: Mario Nebl BREAKING CHANGE Introduces new rules on `error` level that validate the line length of `footer` and `body`. This might create negative linting reports for commits that previously where passing. --- @commitlint/config-conventional/README.md | 94 ++++++++ @commitlint/config-conventional/index.js | 2 + @commitlint/config-conventional/index.test.js | 214 ++++++++++++++++++ @commitlint/config-conventional/package.json | 1 + @commitlint/config-conventional/tsconfig.json | 10 + 5 files changed, 321 insertions(+) create mode 100644 @commitlint/config-conventional/index.test.js create mode 100644 @commitlint/config-conventional/tsconfig.json diff --git a/@commitlint/config-conventional/README.md b/@commitlint/config-conventional/README.md index 60ba13cd35..3cf47604c3 100644 --- a/@commitlint/config-conventional/README.md +++ b/@commitlint/config-conventional/README.md @@ -24,6 +24,7 @@ Consult [docs/rules](https://conventional-changelog.github.io/commitlint/#/refer - **condition**: `type` is found in value - **rule**: `always` +- **level**: `error` - **value** ``` @@ -51,6 +52,7 @@ echo "fix: some message" # passes - **description**: `type` is in case `value` - **rule**: `always` +- **level**: `error` - **value** ``` 'lowerCase' @@ -65,6 +67,7 @@ echo "fix: some message" # passes - **condition**: `type` is empty - **rule**: `never` +- **level**: `error` ```sh echo ": some message" # fails @@ -75,6 +78,7 @@ echo "fix: some message" # passes - **condition**: `scope` is in case `value` - **rule**: `always` +- **level**: `error` ``` 'lowerCase' @@ -89,6 +93,7 @@ echo "fix(scope): some message" # passes - **condition**: `subject` is in one of the cases `['sentence-case', 'start-case', 'pascal-case', 'upper-case']` - **rule**: `never` +- **level**: `error` ```sh echo "fix(SCOPE): Some message" # fails @@ -103,6 +108,7 @@ echo "fix(scope): some Message" # passes - **condition**: `subject` is empty - **rule**: `never` +- **level**: `error` ```sh echo "fix:" # fails @@ -113,6 +119,7 @@ echo "fix: some message" # passes - **condition**: `subject` ends with `value` - **rule**: `never` +- **level**: `error` - **value** ``` @@ -128,6 +135,7 @@ echo "fix: some message" # passes - **condition**: `header` has `value` or less characters - **rule**: `always` +- **level**: `error` - **value** ``` @@ -138,3 +146,89 @@ echo "fix: some message" # passes echo "fix: some message that is way too long and breaks the line max-length by several characters" # fails echo "fix: some message" # passes ``` + +#### footer-leading-blank + +- **condition**: `footer` should have a leading blank line +- **rule**: `always` +- level: `warning` +- **value** + +``` +100 +``` + +```sh +echo "fix: some message +BREAKING CHANGE: It will be significant" # warning + +echo "fix: some message + +BREAKING CHANGE: It will be significant" # passes +``` + +#### footer-max-line-length + +- **condition**: `footer` each line has `value` or less characters +- **rule**: `always` +- level: `error` +- **value** + +``` +100 +``` + +```sh +echo "fix: some message + +BREAKING CHANGE: footer with multiple lines +has a message that is way too long and will break the line rule 'line-max-length' by several characters" # fails + +echo "fix: some message + +BREAKING CHANGE: footer with multiple lines +but still no line is too long" # passes +``` + +#### body-leading-blank + +- **condition**: `body` should have a leading blank line +- **rule**: `always` +- level: `warning` +- **value** + +```js +100; +``` + +```sh +echo "fix: some message +body" # warning + +echo "fix: some message + +body" # passes +``` + +#### body-max-line-length + +- **condition**: `body` each line has `value` or less characters +- **rule**: `always` +- level: `error` +- **value** + +```js +100; +``` + +```sh +echo "fix: some message + +body with multiple lines +has a message that is way too long and will break the line rule 'line-max-length' by several characters" # fails + +echo "fix: some message + +body with multiple lines +but still no line is too long" # passes +``` diff --git a/@commitlint/config-conventional/index.js b/@commitlint/config-conventional/index.js index 2f84ceed08..f7bff45ce8 100644 --- a/@commitlint/config-conventional/index.js +++ b/@commitlint/config-conventional/index.js @@ -2,7 +2,9 @@ module.exports = { parserPreset: 'conventional-changelog-conventionalcommits', rules: { 'body-leading-blank': [1, 'always'], + 'body-max-line-length': [2, 'always', 100], 'footer-leading-blank': [1, 'always'], + 'footer-max-line-length': [2, 'always', 100], 'header-max-length': [2, 'always', 100], 'scope-case': [2, 'always', 'lower-case'], 'subject-case': [ diff --git a/@commitlint/config-conventional/index.test.js b/@commitlint/config-conventional/index.test.js new file mode 100644 index 0000000000..ac0a0c2ecb --- /dev/null +++ b/@commitlint/config-conventional/index.test.js @@ -0,0 +1,214 @@ +import lint from '@commitlint/lint'; +import {rules} from '.'; + +const messages = { + invalidTypeEnum: 'foo: some message', + invalidTypeCase: 'FIX: some message', + invalidTypeEmpty: ': some message', + invalidScopeCase: 'fix(SCOPE): some message', + invalidSubjectCases: [ + 'fix(scope): Some message', + 'fix(scope): Some Message', + 'fix(scope): SomeMessage', + 'fix(scope): SOMEMESSAGE' + ], + invalidSubjectEmpty: 'fix:', + invalidSubjectFullStop: 'fix: some message.', + invalidHeaderMaxLength: + 'fix: some message that is way too long and breaks the line max-length by several characters since the max is 100', + warningFooterLeadingBlank: + 'fix: some message\n\nbody\nBREAKING CHANGE: It will be significant', + invalidFooterMaxLineLength: + 'fix: some message\n\nbody\n\nBREAKING CHANGE: footer with multiple lines\nhas a message that is way too long and will break the line rule "line-max-length" by several characters', + warningBodyLeadingBlank: 'fix: some message\nbody', + invalidBodyMaxLineLength: + 'fix: some message\n\nbody with multiple lines\nhas a message that is way too long and will break the line rule "line-max-length" by several characters', + validMessages: [ + 'fix: some message', + 'fix(scope): some message', + 'fix(scope): some Message', + 'fix(scope): some message\n\nBREAKING CHANGE: it will be significant!', + 'fix(scope): some message\n\nbody' + ] +}; + +const errors = { + typeEnum: { + level: 2, + message: + 'type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]', + name: 'type-enum', + valid: false + }, + typeCase: { + level: 2, + message: 'type must be lower-case', + name: 'type-case', + valid: false + }, + typeEmpty: { + level: 2, + message: 'type may not be empty', + name: 'type-empty', + valid: false + }, + scopeCase: { + level: 2, + message: 'scope must be lower-case', + name: 'scope-case', + valid: false + }, + subjectCase: { + level: 2, + message: + 'subject must not be sentence-case, start-case, pascal-case, upper-case', + name: 'subject-case', + valid: false + }, + subjectEmpty: { + level: 2, + message: 'subject may not be empty', + name: 'subject-empty', + valid: false + }, + subjectFullStop: { + level: 2, + message: 'subject may not end with full stop', + name: 'subject-full-stop', + valid: false + }, + headerMaxLength: { + level: 2, + message: + 'header must not be longer than 100 characters, current length is 112', + name: 'header-max-length', + valid: false + }, + footerMaxLineLength: { + level: 2, + message: "footer's lines must not be longer than 100 characters", + name: 'footer-max-line-length', + valid: false + }, + bodyMaxLineLength: { + level: 2, + message: "body's lines must not be longer than 100 characters", + name: 'body-max-line-length', + valid: false + } +}; + +const warnings = { + footerLeadingBlank: { + level: 1, + message: 'footer must have leading blank line', + name: 'footer-leading-blank', + valid: false + }, + bodyLeadingBlank: { + level: 1, + message: 'body must have leading blank line', + name: 'body-leading-blank', + valid: false + } +}; + +test('type-enum', async () => { + const result = await lint(messages.invalidTypeEnum, rules); + + expect(result.valid).toBe(false); + expect(result.errors).toEqual([errors.typeEnum]); +}); + +test('type-case', async () => { + const result = await lint(messages.invalidTypeCase, rules); + + expect(result.valid).toBe(false); + expect(result.errors).toEqual([errors.typeCase, errors.typeEnum]); +}); + +test('type-empty', async () => { + const result = await lint(messages.invalidTypeEmpty, rules); + + expect(result.valid).toBe(false); + expect(result.errors).toEqual([errors.typeEmpty]); +}); + +test('scope-case', async () => { + const result = await lint(messages.invalidScopeCase, rules); + + expect(result.valid).toBe(false); + expect(result.errors).toEqual([errors.scopeCase]); +}); + +test('subject-case', async () => { + const invalidInputs = await Promise.all( + messages.invalidSubjectCases.map(invalidInput => lint(invalidInput, rules)) + ); + + invalidInputs.forEach(result => { + expect(result.valid).toBe(false); + expect(result.errors).toEqual([errors.subjectCase]); + }); +}); + +test('subject-empty', async () => { + const result = await lint(messages.invalidSubjectEmpty, rules); + + expect(result.valid).toBe(false); + expect(result.errors).toEqual([errors.subjectEmpty, errors.typeEmpty]); +}); + +test('subject-full-stop', async () => { + const result = await lint(messages.invalidSubjectFullStop, rules); + + expect(result.valid).toBe(false); + expect(result.errors).toEqual([errors.subjectFullStop]); +}); + +test('header-max-length', async () => { + const result = await lint(messages.invalidHeaderMaxLength, rules); + + expect(result.valid).toBe(false); + expect(result.errors).toEqual([errors.headerMaxLength]); +}); + +test('footer-leading-blank', async () => { + const result = await lint(messages.warningFooterLeadingBlank, rules); + + expect(result.valid).toBe(true); + expect(result.warnings).toEqual([warnings.footerLeadingBlank]); +}); + +test('footer-max-line-length', async () => { + const result = await lint(messages.invalidFooterMaxLineLength, rules); + + expect(result.valid).toBe(false); + expect(result.errors).toEqual([errors.footerMaxLineLength]); +}); + +test('body-leading-blank', async () => { + const result = await lint(messages.warningBodyLeadingBlank, rules); + + expect(result.valid).toBe(true); + expect(result.warnings).toEqual([warnings.bodyLeadingBlank]); +}); + +test('body-max-line-length', async () => { + const result = await lint(messages.invalidBodyMaxLineLength, rules); + + expect(result.valid).toBe(false); + expect(result.errors).toEqual([errors.bodyMaxLineLength]); +}); + +test('valid messages', async () => { + const validInputs = await Promise.all( + messages.validMessages.map(input => lint(input, rules)) + ); + + validInputs.forEach(result => { + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + expect(result.warnings).toEqual([]); + }); +}); diff --git a/@commitlint/config-conventional/package.json b/@commitlint/config-conventional/package.json index 75832c49e8..3d923233c9 100644 --- a/@commitlint/config-conventional/package.json +++ b/@commitlint/config-conventional/package.json @@ -32,6 +32,7 @@ "node": ">=8" }, "devDependencies": { + "@commitlint/lint": "^8.3.4", "@commitlint/utils": "^8.3.4" }, "dependencies": { diff --git a/@commitlint/config-conventional/tsconfig.json b/@commitlint/config-conventional/tsconfig.json new file mode 100644 index 0000000000..2905f19318 --- /dev/null +++ b/@commitlint/config-conventional/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.shared.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["./**/*.ts"], + "exclude": ["./**/*.test.ts"] +}