Skip to content

Validators

Validators for commit message components.

CommitValidators

Collection of validator methods for different parts of commit messages.

Source code in src/codemap/git/commit_linter/validators.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
class CommitValidators:
	"""Collection of validator methods for different parts of commit messages."""

	@staticmethod
	def validate_footer_token(token: str) -> bool:
		"""
		Validate a footer token according to the Conventional Commits spec.

		According to the spec:
		1. Tokens MUST use hyphens instead of spaces
		2. BREAKING CHANGE must be uppercase
		3. Footer tokens should be ALL UPPERCASE
		4. Footer tokens should follow format with - for spaces
		5. No special characters or Unicode (non-ASCII) characters allowed

		Returns:
		    bool: True if token is valid, False otherwise

		"""
		# Check if token is a breaking change token in any case
		if BREAKING_CHANGE_REGEX.match(token.lower()):
			# If it's a breaking change token, it MUST be uppercase
			return token in (BREAKING_CHANGE, BREAKING_CHANGE_HYPHEN)

		# Check for special characters (except hyphens which are allowed)
		if any(c in token for c in "!@#$%^&*()+={}[]|\\:;\"'<>,./?"):
			return False

		# Check for non-ASCII characters
		if any(ord(c) > ASCII_MAX_VALUE for c in token):
			return False

		# Must match valid token pattern (uppercase, alphanumeric with hyphens)
		if not VALID_FOOTER_TOKEN_REGEX.match(token):
			return False

		# Check for spaces (must use hyphens instead, except for BREAKING CHANGE)
		return not (" " in token and token != BREAKING_CHANGE)

	@staticmethod
	def validate_type_and_scope(type_value: str, scope_value: str | None) -> list[str]:
		"""
		Validate type and scope values according to the spec.

		Type must contain only letters.
		Scope must contain only letters, numbers, hyphens, and slashes.
		Both must be ASCII-only.

		Args:
		    type_value (str): The commit message type
		    scope_value (str | None): The optional scope

		Returns:
		    list[str]: List of error messages, empty if valid

		"""
		errors = []

		# Check type (no special chars or unicode)
		if not VALID_TYPE_REGEX.match(type_value):
			errors.append(f"Invalid type '{type_value}'. Types must contain only letters (a-z, A-Z).")
		elif any(ord(c) > ASCII_MAX_VALUE for c in type_value):
			errors.append(f"Invalid type '{type_value}'. Types must contain only ASCII characters.")

		# Check scope (if present)
		if scope_value is not None:
			if scope_value == "":
				errors.append("Scope cannot be empty when parentheses are used.")
			elif not VALID_SCOPE_REGEX.match(scope_value):
				errors.append(
					f"Invalid scope '{scope_value}'. Scopes must contain only letters, numbers, hyphens, and slashes."
				)
			elif any(ord(c) > ASCII_MAX_VALUE for c in scope_value):
				errors.append(f"Invalid scope '{scope_value}'. Scopes must contain only ASCII characters.")
			elif any(c in scope_value for c in "!@#$%^&*()+={}[]|\\:;\"'<>,. "):
				errors.append(f"Invalid scope '{scope_value}'. Special characters are not allowed in scopes.")

		return errors

	@staticmethod
	def validate_case(text: str, case_format: str | list[str]) -> bool:
		"""
		Validate if the text follows the specified case format.

		Args:
		    text (str): The text to validate
		    case_format (str or list): The case format(s) to check

		Returns:
		    bool: True if text matches any of the specified case formats

		"""
		if isinstance(case_format, list):
			return any(CommitValidators.validate_case(text, fmt) for fmt in case_format)

		# Get the validator function for the specified case format
		validator = CASE_FORMATS.get(case_format)
		if not validator:
			# Default to allowing any case if invalid format specified
			return True

		return validator(text)

	@staticmethod
	def validate_length(text: str | None, min_length: int, max_length: float) -> bool:
		"""
		Validate if text length is between min and max length.

		Args:
		    text (str | None): The text to validate, or None
		    min_length (int): Minimum allowed length
		    max_length (int | float): Maximum allowed length

		Returns:
		    bool: True if text length is valid, False otherwise

		"""
		if text is None:
			return min_length == 0

		text_length = len(text)
		return min_length <= text_length < max_length

	@staticmethod
	def validate_enum(text: str, allowed_values: list[str]) -> bool:
		"""
		Validate if text is in the allowed values.

		Args:
		    text (str): The text to validate
		    allowed_values (list): The allowed values

		Returns:
		    bool: True if text is in allowed values, False otherwise

		"""
		# Allow any value if no allowed values are specified
		if not allowed_values:
			return True

		return text.lower() in (value.lower() for value in allowed_values)

	@staticmethod
	def validate_empty(text: str | None, should_be_empty: bool) -> bool:
		"""
		Validate if text is empty or not based on configuration.

		Args:
		    text (str | None): The text to validate
		    should_be_empty (bool): True if text should be empty, False if not

		Returns:
		    bool: True if text empty status matches should_be_empty

		"""
		is_empty = text is None or text.strip() == ""
		return is_empty == should_be_empty

	@staticmethod
	def validate_ends_with(text: str | None, suffix: str, should_end_with: bool) -> bool:
		"""
		Validate if text ends with a specific suffix.

		Args:
		    text (str | None): The text to validate
		    suffix (str): The suffix to check for
		    should_end_with (bool): True if text should end with suffix

		Returns:
		    bool: True if text ending matches expectation

		"""
		if text is None:
			return not should_end_with

		ends_with = text.endswith(suffix)
		return ends_with == should_end_with

	@staticmethod
	def validate_starts_with(text: str | None, prefix: str, should_start_with: bool) -> bool:
		"""
		Validate if text starts with a specific prefix.

		Args:
		    text (str | None): The text to validate
		    prefix (str): The prefix to check for
		    should_start_with (bool): True if text should start with prefix

		Returns:
		    bool: True if text starting matches expectation

		"""
		if text is None:
			return not should_start_with

		starts_with = text.startswith(prefix)
		return starts_with == should_start_with

	@staticmethod
	def validate_line_length(text: str | None, max_line_length: float) -> list[int]:
		"""
		Validate line lengths in multiline text.

		Args:
		    text (str | None): The text to validate
		    max_line_length (int | float): Maximum allowed line length

		Returns:
		    list: List of line numbers with errors (0-indexed)

		"""
		if text is None or max_line_length == float("inf"):
			return []

		lines = text.splitlines()
		return [i for i, line in enumerate(lines) if len(line) > max_line_length]

	@staticmethod
	def validate_leading_blank(text: str | None, required_blank: bool) -> bool:
		"""
		Validate if text starts with a blank line.

		Args:
		    text (str | None): The text to validate
		    required_blank (bool): True if text should start with blank line

		Returns:
		    bool: True if text leading blank matches expectation

		"""
		if text is None:
			return not required_blank

		lines = text.splitlines()
		has_leading_blank = len(lines) > 0 and (len(lines) == 1 or not lines[0].strip())
		return has_leading_blank == required_blank

	@staticmethod
	def validate_trim(text: str | None) -> bool:
		"""
		Validate if text has no leading/trailing whitespace.

		Args:
		    text (str | None): The text to validate

		Returns:
		    bool: True if text has no leading/trailing whitespace

		"""
		if text is None:
			return True

		return text == text.strip()

	@staticmethod
	def validate_contains(text: str | None, substring: str, should_contain: bool) -> bool:
		"""
		Validate if text contains a specific substring.

		Args:
		    text (str | None): The text to validate
		    substring (str): The substring to check for
		    should_contain (bool): True if text should contain substring

		Returns:
		    bool: True if text contains substring matches expectation

		"""
		if text is None:
			return not should_contain

		contains = substring in text
		return contains == should_contain
validate_footer_token(token: str) -> bool

Validate a footer token according to the Conventional Commits spec.

According to the spec: 1. Tokens MUST use hyphens instead of spaces 2. BREAKING CHANGE must be uppercase 3. Footer tokens should be ALL UPPERCASE 4. Footer tokens should follow format with - for spaces 5. No special characters or Unicode (non-ASCII) characters allowed

Returns:

Name Type Description
bool bool

True if token is valid, False otherwise

Source code in src/codemap/git/commit_linter/validators.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@staticmethod
def validate_footer_token(token: str) -> bool:
	"""
	Validate a footer token according to the Conventional Commits spec.

	According to the spec:
	1. Tokens MUST use hyphens instead of spaces
	2. BREAKING CHANGE must be uppercase
	3. Footer tokens should be ALL UPPERCASE
	4. Footer tokens should follow format with - for spaces
	5. No special characters or Unicode (non-ASCII) characters allowed

	Returns:
	    bool: True if token is valid, False otherwise

	"""
	# Check if token is a breaking change token in any case
	if BREAKING_CHANGE_REGEX.match(token.lower()):
		# If it's a breaking change token, it MUST be uppercase
		return token in (BREAKING_CHANGE, BREAKING_CHANGE_HYPHEN)

	# Check for special characters (except hyphens which are allowed)
	if any(c in token for c in "!@#$%^&*()+={}[]|\\:;\"'<>,./?"):
		return False

	# Check for non-ASCII characters
	if any(ord(c) > ASCII_MAX_VALUE for c in token):
		return False

	# Must match valid token pattern (uppercase, alphanumeric with hyphens)
	if not VALID_FOOTER_TOKEN_REGEX.match(token):
		return False

	# Check for spaces (must use hyphens instead, except for BREAKING CHANGE)
	return not (" " in token and token != BREAKING_CHANGE)

validate_type_and_scope staticmethod

validate_type_and_scope(
	type_value: str, scope_value: str | None
) -> list[str]

Validate type and scope values according to the spec.

Type must contain only letters. Scope must contain only letters, numbers, hyphens, and slashes. Both must be ASCII-only.

Parameters:

Name Type Description Default
type_value str

The commit message type

required
scope_value str | None

The optional scope

required

Returns:

Type Description
list[str]

list[str]: List of error messages, empty if valid

Source code in src/codemap/git/commit_linter/validators.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@staticmethod
def validate_type_and_scope(type_value: str, scope_value: str | None) -> list[str]:
	"""
	Validate type and scope values according to the spec.

	Type must contain only letters.
	Scope must contain only letters, numbers, hyphens, and slashes.
	Both must be ASCII-only.

	Args:
	    type_value (str): The commit message type
	    scope_value (str | None): The optional scope

	Returns:
	    list[str]: List of error messages, empty if valid

	"""
	errors = []

	# Check type (no special chars or unicode)
	if not VALID_TYPE_REGEX.match(type_value):
		errors.append(f"Invalid type '{type_value}'. Types must contain only letters (a-z, A-Z).")
	elif any(ord(c) > ASCII_MAX_VALUE for c in type_value):
		errors.append(f"Invalid type '{type_value}'. Types must contain only ASCII characters.")

	# Check scope (if present)
	if scope_value is not None:
		if scope_value == "":
			errors.append("Scope cannot be empty when parentheses are used.")
		elif not VALID_SCOPE_REGEX.match(scope_value):
			errors.append(
				f"Invalid scope '{scope_value}'. Scopes must contain only letters, numbers, hyphens, and slashes."
			)
		elif any(ord(c) > ASCII_MAX_VALUE for c in scope_value):
			errors.append(f"Invalid scope '{scope_value}'. Scopes must contain only ASCII characters.")
		elif any(c in scope_value for c in "!@#$%^&*()+={}[]|\\:;\"'<>,. "):
			errors.append(f"Invalid scope '{scope_value}'. Special characters are not allowed in scopes.")

	return errors

validate_case staticmethod

validate_case(
	text: str, case_format: str | list[str]
) -> bool

Validate if the text follows the specified case format.

Parameters:

Name Type Description Default
text str

The text to validate

required
case_format str or list

The case format(s) to check

required

Returns:

Name Type Description
bool bool

True if text matches any of the specified case formats

Source code in src/codemap/git/commit_linter/validators.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
@staticmethod
def validate_case(text: str, case_format: str | list[str]) -> bool:
	"""
	Validate if the text follows the specified case format.

	Args:
	    text (str): The text to validate
	    case_format (str or list): The case format(s) to check

	Returns:
	    bool: True if text matches any of the specified case formats

	"""
	if isinstance(case_format, list):
		return any(CommitValidators.validate_case(text, fmt) for fmt in case_format)

	# Get the validator function for the specified case format
	validator = CASE_FORMATS.get(case_format)
	if not validator:
		# Default to allowing any case if invalid format specified
		return True

	return validator(text)

validate_length staticmethod

validate_length(
	text: str | None, min_length: int, max_length: float
) -> bool

Validate if text length is between min and max length.

Parameters:

Name Type Description Default
text str | None

The text to validate, or None

required
min_length int

Minimum allowed length

required
max_length int | float

Maximum allowed length

required

Returns:

Name Type Description
bool bool

True if text length is valid, False otherwise

Source code in src/codemap/git/commit_linter/validators.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
@staticmethod
def validate_length(text: str | None, min_length: int, max_length: float) -> bool:
	"""
	Validate if text length is between min and max length.

	Args:
	    text (str | None): The text to validate, or None
	    min_length (int): Minimum allowed length
	    max_length (int | float): Maximum allowed length

	Returns:
	    bool: True if text length is valid, False otherwise

	"""
	if text is None:
		return min_length == 0

	text_length = len(text)
	return min_length <= text_length < max_length

validate_enum staticmethod

validate_enum(text: str, allowed_values: list[str]) -> bool

Validate if text is in the allowed values.

Parameters:

Name Type Description Default
text str

The text to validate

required
allowed_values list

The allowed values

required

Returns:

Name Type Description
bool bool

True if text is in allowed values, False otherwise

Source code in src/codemap/git/commit_linter/validators.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
@staticmethod
def validate_enum(text: str, allowed_values: list[str]) -> bool:
	"""
	Validate if text is in the allowed values.

	Args:
	    text (str): The text to validate
	    allowed_values (list): The allowed values

	Returns:
	    bool: True if text is in allowed values, False otherwise

	"""
	# Allow any value if no allowed values are specified
	if not allowed_values:
		return True

	return text.lower() in (value.lower() for value in allowed_values)

validate_empty staticmethod

validate_empty(
	text: str | None, should_be_empty: bool
) -> bool

Validate if text is empty or not based on configuration.

Parameters:

Name Type Description Default
text str | None

The text to validate

required
should_be_empty bool

True if text should be empty, False if not

required

Returns:

Name Type Description
bool bool

True if text empty status matches should_be_empty

Source code in src/codemap/git/commit_linter/validators.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
@staticmethod
def validate_empty(text: str | None, should_be_empty: bool) -> bool:
	"""
	Validate if text is empty or not based on configuration.

	Args:
	    text (str | None): The text to validate
	    should_be_empty (bool): True if text should be empty, False if not

	Returns:
	    bool: True if text empty status matches should_be_empty

	"""
	is_empty = text is None or text.strip() == ""
	return is_empty == should_be_empty

validate_ends_with staticmethod

validate_ends_with(
	text: str | None, suffix: str, should_end_with: bool
) -> bool

Validate if text ends with a specific suffix.

Parameters:

Name Type Description Default
text str | None

The text to validate

required
suffix str

The suffix to check for

required
should_end_with bool

True if text should end with suffix

required

Returns:

Name Type Description
bool bool

True if text ending matches expectation

Source code in src/codemap/git/commit_linter/validators.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
@staticmethod
def validate_ends_with(text: str | None, suffix: str, should_end_with: bool) -> bool:
	"""
	Validate if text ends with a specific suffix.

	Args:
	    text (str | None): The text to validate
	    suffix (str): The suffix to check for
	    should_end_with (bool): True if text should end with suffix

	Returns:
	    bool: True if text ending matches expectation

	"""
	if text is None:
		return not should_end_with

	ends_with = text.endswith(suffix)
	return ends_with == should_end_with

validate_starts_with staticmethod

validate_starts_with(
	text: str | None, prefix: str, should_start_with: bool
) -> bool

Validate if text starts with a specific prefix.

Parameters:

Name Type Description Default
text str | None

The text to validate

required
prefix str

The prefix to check for

required
should_start_with bool

True if text should start with prefix

required

Returns:

Name Type Description
bool bool

True if text starting matches expectation

Source code in src/codemap/git/commit_linter/validators.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
@staticmethod
def validate_starts_with(text: str | None, prefix: str, should_start_with: bool) -> bool:
	"""
	Validate if text starts with a specific prefix.

	Args:
	    text (str | None): The text to validate
	    prefix (str): The prefix to check for
	    should_start_with (bool): True if text should start with prefix

	Returns:
	    bool: True if text starting matches expectation

	"""
	if text is None:
		return not should_start_with

	starts_with = text.startswith(prefix)
	return starts_with == should_start_with

validate_line_length staticmethod

validate_line_length(
	text: str | None, max_line_length: float
) -> list[int]

Validate line lengths in multiline text.

Parameters:

Name Type Description Default
text str | None

The text to validate

required
max_line_length int | float

Maximum allowed line length

required

Returns:

Name Type Description
list list[int]

List of line numbers with errors (0-indexed)

Source code in src/codemap/git/commit_linter/validators.py
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
@staticmethod
def validate_line_length(text: str | None, max_line_length: float) -> list[int]:
	"""
	Validate line lengths in multiline text.

	Args:
	    text (str | None): The text to validate
	    max_line_length (int | float): Maximum allowed line length

	Returns:
	    list: List of line numbers with errors (0-indexed)

	"""
	if text is None or max_line_length == float("inf"):
		return []

	lines = text.splitlines()
	return [i for i, line in enumerate(lines) if len(line) > max_line_length]

validate_leading_blank staticmethod

validate_leading_blank(
	text: str | None, required_blank: bool
) -> bool

Validate if text starts with a blank line.

Parameters:

Name Type Description Default
text str | None

The text to validate

required
required_blank bool

True if text should start with blank line

required

Returns:

Name Type Description
bool bool

True if text leading blank matches expectation

Source code in src/codemap/git/commit_linter/validators.py
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
@staticmethod
def validate_leading_blank(text: str | None, required_blank: bool) -> bool:
	"""
	Validate if text starts with a blank line.

	Args:
	    text (str | None): The text to validate
	    required_blank (bool): True if text should start with blank line

	Returns:
	    bool: True if text leading blank matches expectation

	"""
	if text is None:
		return not required_blank

	lines = text.splitlines()
	has_leading_blank = len(lines) > 0 and (len(lines) == 1 or not lines[0].strip())
	return has_leading_blank == required_blank

validate_trim staticmethod

validate_trim(text: str | None) -> bool

Validate if text has no leading/trailing whitespace.

Parameters:

Name Type Description Default
text str | None

The text to validate

required

Returns:

Name Type Description
bool bool

True if text has no leading/trailing whitespace

Source code in src/codemap/git/commit_linter/validators.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
@staticmethod
def validate_trim(text: str | None) -> bool:
	"""
	Validate if text has no leading/trailing whitespace.

	Args:
	    text (str | None): The text to validate

	Returns:
	    bool: True if text has no leading/trailing whitespace

	"""
	if text is None:
		return True

	return text == text.strip()

validate_contains staticmethod

validate_contains(
	text: str | None, substring: str, should_contain: bool
) -> bool

Validate if text contains a specific substring.

Parameters:

Name Type Description Default
text str | None

The text to validate

required
substring str

The substring to check for

required
should_contain bool

True if text should contain substring

required

Returns:

Name Type Description
bool bool

True if text contains substring matches expectation

Source code in src/codemap/git/commit_linter/validators.py
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
@staticmethod
def validate_contains(text: str | None, substring: str, should_contain: bool) -> bool:
	"""
	Validate if text contains a specific substring.

	Args:
	    text (str | None): The text to validate
	    substring (str): The substring to check for
	    should_contain (bool): True if text should contain substring

	Returns:
	    bool: True if text contains substring matches expectation

	"""
	if text is None:
		return not should_contain

	contains = substring in text
	return contains == should_contain