UNPKG

@dev-build-deploy/commit-it

Version:
311 lines (310 loc) 14.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.commitRules = void 0; /* * SPDX-FileCopyrightText: 2023 Kevin de Jong <monkaii@hotmail.com> * * SPDX-License-Identifier: MIT * SPDX-License-Identifier: CC-BY-3.0 */ const diagnose_it_1 = require("@dev-build-deploy/diagnose-it"); const chalk_1 = __importDefault(require("chalk")); const commit_1 = require("./commit"); /** * Checks whether the provided string is a noun. * A noun is defined as a single word which can be capitalized or contain hyphens, therefore * it will not support multi-word nouns (i.e. New York). * * @param str String to check * @returns True if the string is a noun, false otherwise. * * @internal */ function isNoun(str) { return /^[A-Za-z][a-z]*(-[A-Za-z][a-z]*)*$/.test(str); } /** * Validates whether the provided type valid. * A valid type is defined as a single word, all lowercase, no spaces, and no special characters. * * @param str String to check * @returns True if the string is a valid type, false otherwise. * * @internal */ function isValidType(str) { return !str.trim().includes(" ") && !/[^a-z]/i.test(str.trim()); } function highlightString(str, substring) { // Ensure that we handle both single and multiple substrings equally if (!Array.isArray(substring)) substring = [substring]; // Replace all instances of substring with a blue version let result = str; substring.forEach(sub => (result = result.replace(sub, `${chalk_1.default.cyan(sub)}`))); return result; } function createDiagnosticsMessage(commit, description, highlight, type, whitespace = false, level = diagnose_it_1.DiagnosticsLevelEnum.Error) { const element = commit[type]; let hintIndex = element.index; let hintLength = element.value?.trimEnd().length ?? 1; if (whitespace) { let prevElement = undefined; for (const [_key, value] of Object.entries(commit)) { if (value.index > (prevElement?.index ?? 0) && value.index < element.index) { prevElement = value; } } hintIndex = prevElement ? prevElement.index + (prevElement.value?.trimEnd().length ?? 1) : 1; hintLength = (prevElement?.value?.length ?? 1) - (prevElement?.value?.trimEnd().length ?? 1); } return new diagnose_it_1.DiagnosticsMessage({ file: commit.commit.hash, level, message: { text: highlightString(description, highlight), linenumber: 1, column: hintIndex, }, }) .setContext(1, commit.commit.subject.split(/\r?\n/)[0]) .addFixitHint(diagnose_it_1.FixItHint.create({ index: hintIndex, length: hintLength === 0 ? 1 : hintLength })); } /** * Commits MUST be prefixed with a type, which consists of a noun, feat, fix, etc., * followed by the OPTIONAL scope, OPTIONAL !, and REQUIRED terminal colon and space. */ class CC01 { id = "CC-01"; description = "Commits MUST be prefixed with a type, which consists of a noun, feat, fix, etc., followed by the OPTIONAL scope, OPTIONAL !, and REQUIRED terminal colon and space."; validate(commit, _options) { const errors = []; // MUST be prefixed with a type if (!commit.type.value || commit.type.value.trim().length === 0) { // Validated with EC-02 } else { // Ensure that we have a valid type (single word, no spaces, no special characters) if (!isValidType(commit.type.value)) { errors.push(createDiagnosticsMessage(commit, this.description, "which consists of a noun", "type")); } // Validate for spacing after the type if (commit.type.value.trim() !== commit.type.value) { if (commit.scope.value) { errors.push(createDiagnosticsMessage(commit, this.description, "followed by the OPTIONAL scope", "scope", true)); } else if (commit.breaking.value) { errors.push(createDiagnosticsMessage(commit, this.description, ["followed by the", "OPTIONAL !"], "breaking", true)); } else { errors.push(createDiagnosticsMessage(commit, this.description, ["followed by the", "REQUIRED terminal colon"], "seperator", true)); } } // Validate for spacing after the scope, breaking and seperator if (commit.scope.value && commit.scope.value.trim() !== commit.scope.value) { if (commit.breaking.value) { errors.push(createDiagnosticsMessage(commit, this.description, ["followed by the", "OPTIONAL !"], "breaking", true)); } else { errors.push(createDiagnosticsMessage(commit, this.description, ["followed by the", "REQUIRED terminal colon"], "seperator", true)); } } if (commit.breaking.value && commit.breaking.value.trim() !== commit.breaking.value) { errors.push(createDiagnosticsMessage(commit, this.description, ["followed by the", "REQUIRED terminal colon"], "seperator", true)); } } // MUST have a terminal colon if (!commit.seperator.value) { errors.push(createDiagnosticsMessage(commit, this.description, ["followed by the", "REQUIRED terminal colon"], "seperator")); } return errors; } } /** * A scope MAY be provided after a type. A scope MUST consist of a noun describing * a section of the codebase surrounded by parenthesis, e.g., fix(parser): */ class CC04 { id = "CC-04"; description = "A scope MAY be provided after a type. A scope MUST consist of a noun describing a section of the codebase surrounded by parenthesis, e.g., fix(parser):"; validate(commit, _options) { const errors = []; if (commit.scope.value && (commit.scope.value === "()" || !isNoun(commit.scope.value.trimEnd().substring(1, commit.scope.value.trimEnd().length - 1)))) { errors.push(createDiagnosticsMessage(commit, this.description, "A scope MUST consist of a noun", "scope")); } return errors; } } /** * A description MUST immediately follow the colon and space after the type/scope prefix. * The description is a short summary of the code changes, e.g., fix: array parsing issue * when multiple spaces were contained in string. */ class CC05 { id = "CC-05"; description = "A description MUST immediately follow the colon and space after the type/scope prefix. The description is a short summary of the code changes, e.g., fix: array parsing issue when multiple spaces were contained in string."; validate(commit, _options) { const errors = []; if (!commit.seperator.value) { return errors; } if (commit.description.value === undefined || commit.seperator.value.length - commit.seperator.value.trim().length !== 1) { errors.push(createDiagnosticsMessage(commit, this.description, "A description MUST immediately follow the colon and space", "description", true)); } return errors; } } /** * A longer commit body MAY be provided after the short description, providing * additional contextual information about the code changes. The body MUST begin one * blank line after the description. */ class CC06 { id = "CC-06"; description = "A longer commit body MAY be provided after the short description, providing additional contextual information about the code changes. The body MUST begin one blank line after the description."; validate(commit, _options) { const errors = []; if (!commit.commit.subject) { return errors; } const lines = commit.commit.subject.split(/\r?\n/); if (lines.length > 1) { return [ diagnose_it_1.DiagnosticsMessage.createError(commit.commit.hash, { text: highlightString(this.description, "The body MUST begin one blank line after the description"), linenumber: 2, column: 1, }) .setContext(1, lines) .addFixitHint(diagnose_it_1.FixItHint.createRemoval({ index: 1, length: lines[1].length })), ]; } return errors; } } /** * The units of information that make up Conventional Commits MUST NOT be treated as case * sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase. */ class CC15 { id = "CC-15"; description = "The units of information that make up Conventional Commits MUST NOT be treated as case sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase."; validate(commit, _options) { const errors = []; const footerElements = (0, commit_1.getFooterElementsFromParagraph)(commit.commit.raw); if (footerElements === undefined) return errors; for (const element of footerElements) { if (["BREAKING CHANGE", "BREAKING-CHANGE"].includes(element.key.toUpperCase())) { if (element.key !== element.key.toUpperCase()) { errors.push(diagnose_it_1.DiagnosticsMessage.createError(commit.commit.hash, { text: highlightString(this.description, "BREAKING CHANGE MUST be uppercase"), linenumber: element.lineNumber, column: 1, }) .setContext(element.lineNumber, commit.commit.raw.split(/\r?\n/)[element.lineNumber - 1]) .addFixitHint(diagnose_it_1.FixItHint.create({ index: 1, length: element.key.length }))); } } } return errors; } } /** * A scope MAY be provided after a type. A scope MUST consist of one of the configured values (...) surrounded by parenthesis */ class EC01 { id = "EC-01"; description = "A scope MAY be provided after a type. A scope MUST consist of one of the configured values (...) surrounded by parenthesis"; validate(commit, options) { const uniqueScopeList = Array.from(new Set(options?.scopes ?? [])); if (uniqueScopeList.length === 0) { return []; } if (commit.scope.value === undefined || uniqueScopeList.includes(commit.scope.value.replace(/[()]+/g, ""))) { return []; } this.description = `A scope MAY be provided after a type. A scope MUST consist of one of the configured values (${uniqueScopeList.join(", ")}) surrounded by parenthesis`; return [ createDiagnosticsMessage(commit, this.description, ["A scope MUST consist of", `(${uniqueScopeList.join(", ")})`], "scope"), ]; } } /** * Commits MUST be prefixed with a type, which consists of one of the configured values (feat, fix, ...) */ class EC02 { id = "EC-02"; description = "Commits MUST be prefixed with a type, which consists of one of the configured values (feat, fix, ...)"; validate(commit, options) { const uniqueAddedTypes = new Set(options?.types ?? []); if (uniqueAddedTypes.has("feat")) uniqueAddedTypes.delete("feat"); if (uniqueAddedTypes.has("fix")) uniqueAddedTypes.delete("fix"); const expectedTypes = ["feat", "fix", ...Array.from(uniqueAddedTypes)]; this.description = `Commits ${uniqueAddedTypes.size > 0 ? "MUST" : "MAY"} be prefixed with a type, which consists of one of the configured values (${expectedTypes.join(", ")}).`; if (commit.type.value === undefined || !isValidType(commit.type.value) || expectedTypes.includes(commit.type.value.toLowerCase().trimEnd())) { return []; } if (commit.type.value.trim().length === 0) { return [createDiagnosticsMessage(commit, this.description, "prefixed with a type", "type")]; } if (uniqueAddedTypes.size > 0) { return [ createDiagnosticsMessage(commit, this.description, ["prefixed with a type, which consists of", `(${expectedTypes.join(", ")})`], "type"), ]; } else { return [ createDiagnosticsMessage(commit, this.description, ["prefixed with a type, which consists of", `(${expectedTypes.join(", ")})`], "type", false, diagnose_it_1.DiagnosticsLevelEnum.Warning), ]; } } } /** * A `BREAKING CHANGE` git-trailer has been found in the body of the commit message and will be ignored as it MUST be included in the footer. */ class WA01 { id = "WA-01"; description = "A `BREAKING CHANGE` git-trailer has been found in the body of the commit message and will be ignored as it MUST be included in the footer."; validate(commit, _options) { const errors = []; if (commit.commit.body === undefined) return errors; const elements = (0, commit_1.getFooterElementsFromParagraph)(commit.commit.body); if (elements === undefined) return errors; for (const element of elements) { if (element.key === "BREAKING CHANGE" || element.key === "BREAKING-CHANGE") { errors.push(diagnose_it_1.DiagnosticsMessage.createWarning(commit.commit.hash, { text: highlightString(`A \`${element.key}\` git-trailer has been found in the body of the commit message and will be ignored as it MUST be included in the footer.`, [element.key, "will be ignored as it MUST be included in the footer"]), linenumber: commit.commit.subject.split(/\r?\n/).length + element.lineNumber, column: 1, }) .setContext(commit.commit.subject.split(/\r?\n/).length + 1, commit.commit.body.split(/\r?\n/)) .addFixitHint(diagnose_it_1.FixItHint.create({ index: 1, length: element.key.length }))); } } return errors; } } /** @internal */ exports.commitRules = [ new CC01(), new CC04(), new CC05(), new CC06(), new CC15(), new EC01(), new EC02(), new WA01(), ];