Skip to content

Commit 8f73143

Browse files
committed
fix: add support for whitespace highlighting in error messages
This commit will improve the error messages generated for the different use cases where Conventional Commit elements are seperated by an incorrect amount of spacing. Instead of placing the FixIt hints on the next element, it will now correctly cover the added (and missing) white spacing.
1 parent 18e4ea7 commit 8f73143

3 files changed

Lines changed: 170 additions & 125 deletions

File tree

src/conventional_commit.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ export interface IRawConventionalCommit {
5151
scope: IConventionalCommitElement;
5252
breaking: IConventionalCommitElement;
5353
seperator: IConventionalCommitElement;
54-
spacing: IConventionalCommitElement;
5554
description: IConventionalCommitElement;
5655
body: IConventionalCommitElement;
5756
}
@@ -143,7 +142,7 @@ export function isConventionalCommit(commit: ICommit | IConventionalCommit): boo
143142
*/
144143
export function getConventionalCommit(commit: ICommit, options?: IConventionalCommitOptions): IConventionalCommit {
145144
const ConventionalCommitRegex = new RegExp(
146-
/^(?<type>[^(!:]*)(?<scope>\([^)]*\))?(?<breaking>\s*!)?(?<separator>\s*:)?(?<spacing>\s*)(?<subject>.*)?$/
145+
/^(?<type>[^(!:]*)(?<scope>\([^)]*\)\s*)?(?<breaking>!\s*)?(?<separator>:\s*)?(?<subject>.*)?$/
147146
);
148147

149148
const match = ConventionalCommitRegex.exec(commit.subject);
@@ -153,7 +152,6 @@ export function getConventionalCommit(commit: ICommit, options?: IConventionalCo
153152
scope: { index: 1, value: match?.groups?.scope },
154153
breaking: { index: 1, value: match?.groups?.breaking },
155154
seperator: { index: 1, value: match?.groups?.separator },
156-
spacing: { index: 1, value: match?.groups?.spacing },
157155
description: { index: 1, value: match?.groups?.subject },
158156
body: { index: 1, value: commit.body },
159157
};
@@ -162,8 +160,7 @@ export function getConventionalCommit(commit: ICommit, options?: IConventionalCo
162160
commit.scope.index = commit.type.index + (commit.type.value?.length ?? 0);
163161
commit.breaking.index = commit.scope.index + (commit.scope.value?.length ?? 0);
164162
commit.seperator.index = commit.breaking.index + (commit.breaking.value?.length ?? 0);
165-
commit.spacing.index = commit.seperator.index + (commit.seperator.value?.length ?? 0);
166-
commit.description.index = commit.spacing.index + (commit.spacing.value?.length ?? 0);
163+
commit.description.index = commit.seperator.index + (commit.seperator.value?.length ?? 0);
167164
return commit;
168165
}
169166

src/requirements.ts

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ interface ICommitRequirement {
2828
validate(commit: IRawConventionalCommit, options?: IConventionalCommitOptions): DiagnosticsMessage[];
2929
}
3030

31+
function isNoun(str: string): boolean {
32+
return !str.trim().includes(" ") && !/[^a-z]/i.test(str.trim());
33+
}
34+
3135
function highlightString(str: string, substring: string | string[]): string {
3236
// Ensure that we handle both single and multiple substrings equally
3337
if (!Array.isArray(substring)) substring = [substring];
@@ -42,22 +46,37 @@ function createError(
4246
commit: IRawConventionalCommit,
4347
description: string,
4448
highlight: string | string[],
45-
type: "type" | "scope" | "breaking" | "seperator" | "spacing" | "description"
49+
type: keyof IRawConventionalCommit,
50+
whitespace = false
4651
): DiagnosticsMessage {
47-
const data = commit[type.toString() as keyof IRawConventionalCommit] as IConventionalCommitElement;
52+
const element = commit[type] as IConventionalCommitElement;
53+
let hintIndex = element.index;
54+
let hintLength = element.value?.trimEnd().length ?? 1;
55+
56+
if (whitespace) {
57+
let prevElement: IConventionalCommitElement | undefined = undefined;
58+
for (const [_key, value] of Object.entries(commit)) {
59+
if (value.index > (prevElement?.index ?? 0) && value.index < element.index) {
60+
prevElement = value;
61+
}
62+
}
63+
64+
hintIndex = prevElement ? prevElement.index + (prevElement.value?.trimEnd().length ?? 1) : 1;
65+
hintLength = (prevElement?.value?.length ?? 1) - (prevElement?.value?.trimEnd().length ?? 1);
66+
}
4867

4968
return DiagnosticsMessage.createError(commit.commit.hash, {
5069
text: highlightString(description, highlight),
5170
linenumber: 1,
52-
column: data.index,
71+
column: hintIndex,
5372
})
5473
.setContext(
5574
1,
5675
commit.commit.body !== undefined && commit.commit.body.split("\n").length >= 1
5776
? [commit.commit.subject, "", ...commit.commit.body.split("\n")]
5877
: [commit.commit.subject]
5978
)
60-
.addFixitHint(FixItHint.create({ index: data.index, length: data.value?.length ?? 1 }));
79+
.addFixitHint(FixItHint.create({ index: hintIndex, length: hintLength === 0 ? 1 : hintLength }));
6180
}
6281

6382
/**
@@ -69,44 +88,47 @@ class CC01 implements ICommitRequirement {
6988
description =
7089
"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.";
7190

72-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
7391
validate(commit: IRawConventionalCommit, _options?: IConventionalCommitOptions): DiagnosticsMessage[] {
7492
const errors: DiagnosticsMessage[] = [];
7593

7694
// MUST be prefixed with a type
7795
if (!commit.type.value || commit.type.value.trim().length === 0) {
78-
errors.push(createError(commit, this.description, "MUST be prefixed with a type", "type"));
96+
// Validated with EC-02
7997
} else {
8098
// Ensure that we have a noun
81-
if (commit.type.value.trim().includes(" ") || /[^a-z]/i.test(commit.type.value.trim()))
99+
if (!isNoun(commit.type.value))
82100
errors.push(createError(commit, this.description, "which consists of a noun", "type"));
83101
// Validate for spacing after the type
84102
if (commit.type.value.trim() !== commit.type.value) {
85103
if (commit.scope.value)
86-
errors.push(createError(commit, this.description, "followed by the OPTIONAL scope", "scope"));
104+
errors.push(createError(commit, this.description, "followed by the OPTIONAL scope", "scope", true));
87105
else if (commit.breaking.value)
88-
errors.push(createError(commit, this.description, ["followed by the", "OPTIONAL !"], "breaking"));
106+
errors.push(createError(commit, this.description, ["followed by the", "OPTIONAL !"], "breaking", true));
89107
else
90108
errors.push(
91-
createError(commit, this.description, ["followed by the", "REQUIRED terminal colon"], "seperator")
109+
createError(commit, this.description, ["followed by the", "REQUIRED terminal colon"], "seperator", true)
92110
);
93111
}
94112

95113
// Validate for spacing after the scope, breaking and seperator
96-
if (commit.scope.value && commit.scope.value.trim() !== commit.scope.value)
97-
errors.push(createError(commit, this.description, "followed by the OPTIONAL scope", "scope"));
114+
if (commit.scope.value && commit.scope.value.trim() !== commit.scope.value) {
115+
if (commit.breaking.value)
116+
errors.push(createError(commit, this.description, ["followed by the", "OPTIONAL !"], "breaking", true));
117+
else
118+
errors.push(
119+
createError(commit, this.description, ["followed by the", "REQUIRED terminal colon"], "seperator", true)
120+
);
121+
}
122+
98123
if (commit.breaking.value && commit.breaking.value.trim() !== commit.breaking.value)
99-
errors.push(createError(commit, this.description, ["followed by the", "OPTIONAL !"], "breaking"));
100-
if (commit.seperator.value && commit.seperator.value.trim() !== commit.seperator.value)
101-
errors.push(createError(commit, this.description, ["followed by the", "REQUIRED terminal colon"], "seperator"));
124+
errors.push(
125+
createError(commit, this.description, ["followed by the", "REQUIRED terminal colon"], "seperator", true)
126+
);
102127
}
103128

104129
// MUST have a terminal colon
105130
if (!commit.seperator.value)
106131
errors.push(createError(commit, this.description, ["followed by the", "REQUIRED terminal colon"], "seperator"));
107-
// MUST have a space after the terminal colon
108-
else if (!commit.spacing.value || commit.spacing.value.length !== 1)
109-
errors.push(createError(commit, this.description, ["followed by the", "REQUIRED", "space"], "spacing"));
110132

111133
return errors;
112134
}
@@ -121,15 +143,13 @@ class CC04 implements ICommitRequirement {
121143
description =
122144
"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):";
123145

124-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
125146
validate(commit: IRawConventionalCommit, _options?: IConventionalCommitOptions): DiagnosticsMessage[] {
126147
const errors: DiagnosticsMessage[] = [];
127148

128149
if (
129150
commit.scope.value &&
130-
(commit.scope.value.includes(" ") ||
131-
commit.scope.value === "()" ||
132-
/[^a-z]/i.test(commit.scope.value.substring(1, commit.scope.value.length - 1)))
151+
(commit.scope.value === "()" ||
152+
!isNoun(commit.scope.value.trimEnd().substring(1, commit.scope.value.trimEnd().length - 1)))
133153
) {
134154
errors.push(createError(commit, this.description, "A scope MUST consist of a noun", "scope"));
135155
}
@@ -148,18 +168,21 @@ class CC05 implements ICommitRequirement {
148168
description =
149169
"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.";
150170

151-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
152171
validate(commit: IRawConventionalCommit, _options?: IConventionalCommitOptions): DiagnosticsMessage[] {
153172
const errors: DiagnosticsMessage[] = [];
154173

155174
if (!commit.seperator.value) return errors;
156-
if (!commit.spacing.value || commit.spacing.value.length > 1 || !commit.description.value)
175+
if (
176+
commit.description.value === undefined ||
177+
commit.seperator.value.length - commit.seperator.value.trim().length !== 1
178+
)
157179
errors.push(
158180
createError(
159181
commit,
160182
this.description,
161183
"A description MUST immediately follow the colon and space",
162-
"description"
184+
"description",
185+
true
163186
)
164187
);
165188

@@ -209,7 +232,16 @@ class EC02 implements ICommitRequirement {
209232
", "
210233
)}).`;
211234

212-
if (commit.type.value !== undefined && expectedTypes.includes(commit.type.value)) return [];
235+
if (
236+
commit.type.value === undefined ||
237+
!isNoun(commit.type.value) ||
238+
expectedTypes.includes(commit.type.value.trimEnd())
239+
)
240+
return [];
241+
242+
if (commit.type.value.trim().length === 0) {
243+
return [createError(commit, this.description, "prefixed with a type", "type")];
244+
}
213245

214246
return [
215247
createError(

0 commit comments

Comments
 (0)