CLASPLint 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CLASPLint-0.2.0/CLASPLint/__init__.py +34 -0
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint/__main__.py +51 -2
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint/comment_checker.py +157 -28
- CLASPLint-0.2.0/CLASPLint/dict_key_checker.py +186 -0
- CLASPLint-0.2.0/CLASPLint/docstring_checker.py +436 -0
- CLASPLint-0.2.0/CLASPLint/function_checker.py +265 -0
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint/log_checker.py +158 -9
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint/naming_utils.py +149 -6
- CLASPLint-0.2.0/CLASPLint/reporter.py +186 -0
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint/runner.py +54 -1
- CLASPLint-0.2.0/CLASPLint/variable_checker.py +339 -0
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint.egg-info/PKG-INFO +16 -13
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint.egg-info/SOURCES.txt +2 -0
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/LICENSE +4 -3
- CLASPLint-0.2.0/MANIFEST.in +1 -0
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/PKG-INFO +16 -13
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/README.md +14 -10
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/pyproject.toml +2 -3
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/setup.py +1 -2
- CLASPLint-0.1.0/CLASPLint/__init__.py +0 -6
- CLASPLint-0.1.0/CLASPLint/dict_key_checker.py +0 -87
- CLASPLint-0.1.0/CLASPLint/function_checker.py +0 -95
- CLASPLint-0.1.0/CLASPLint/reporter.py +0 -90
- CLASPLint-0.1.0/CLASPLint/variable_checker.py +0 -159
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint.egg-info/dependency_links.txt +0 -0
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint.egg-info/entry_points.txt +0 -0
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint.egg-info/top_level.txt +0 -0
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint.egg-info/zip-safe +0 -0
- {CLASPLint-0.1.0 → CLASPLint-0.2.0}/setup.cfg +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
THIS FILE IS PART OF CLASPLINT BY MATT BELFAST BROWN
|
|
3
|
+
CLASPLint.__init__ — Package entry point and public API surface.
|
|
4
|
+
|
|
5
|
+
Exposes the runner and reporter modules as the primary public interface for
|
|
6
|
+
programmatic use of the CLASP 3.0 / PEP 2606 static analysis engine.
|
|
7
|
+
|
|
8
|
+
Author: Matt Belfast Brown
|
|
9
|
+
Create Date: 2026-06-17
|
|
10
|
+
Version Date: 2026-06-21
|
|
11
|
+
Version: 0.2.0
|
|
12
|
+
|
|
13
|
+
THIS PROGRAM IS LICENSED UNDER GPL-3.0
|
|
14
|
+
YOU SHOULD HAVE RECEIVED A COPY OF GPL-3.0 LICENSE.
|
|
15
|
+
|
|
16
|
+
Copyright (C) 2026 Matt Belfast Brown
|
|
17
|
+
|
|
18
|
+
This program is free software: you can redistribute it and/or modify
|
|
19
|
+
it under the terms of the GNU General Public License as published by
|
|
20
|
+
the Free Software Foundation, version 3 of the License.
|
|
21
|
+
|
|
22
|
+
This program is distributed in the hope that it will be useful,
|
|
23
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
24
|
+
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
25
|
+
GNU General Public License for more details.
|
|
26
|
+
|
|
27
|
+
You should have received a copy of the GNU General Public License
|
|
28
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# Define the package version string.
|
|
32
|
+
__version__ = "0.2.0"
|
|
33
|
+
# Expose the runner and reporter modules as the public API.
|
|
34
|
+
__all__ = ["runner", "reporter"]
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
THIS FILE IS PART OF CLASPLINT BY MATT BELFAST BROWN
|
|
3
|
+
CLASPLint.__main__ — command-line entry point for the CLASP 3.0 static analysis tool.
|
|
4
|
+
|
|
5
|
+
Author: Matt Belfast Brown
|
|
6
|
+
Create Date: 2026-06-17
|
|
7
|
+
Version Date: 2026-06-21
|
|
8
|
+
Version: 0.2.0
|
|
9
|
+
|
|
10
|
+
THIS PROGRAM IS LICENSED UNDER GPL-3.0
|
|
11
|
+
YOU SHOULD HAVE RECEIVED A COPY OF GPL-3.0 LICENSE.
|
|
12
|
+
|
|
13
|
+
Copyright (C) 2026 Matt Belfast Brown
|
|
14
|
+
|
|
15
|
+
This program is free software: you can redistribute it and/or modify
|
|
16
|
+
it under the terms of the GNU General Public License as published by
|
|
17
|
+
the Free Software Foundation, version 3 of the License.
|
|
18
|
+
|
|
19
|
+
This program is distributed in the hope that it will be useful,
|
|
20
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
21
|
+
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
22
|
+
GNU General Public License for more details.
|
|
23
|
+
|
|
24
|
+
You should have received a copy of the GNU General Public License
|
|
25
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
26
|
+
"""
|
|
2
27
|
|
|
3
28
|
import argparse
|
|
4
29
|
import sys
|
|
@@ -12,36 +37,57 @@ def _init_build_parser_function_() -> argparse.ArgumentParser:
|
|
|
12
37
|
"""Construct the argument parser for the CLASPLint CLI."""
|
|
13
38
|
# Create the top-level argument parser with a description.
|
|
14
39
|
parser_result = argparse.ArgumentParser(
|
|
40
|
+
# Set the program name displayed in help text.
|
|
15
41
|
prog="CLASPLint",
|
|
42
|
+
# Provide a description of what the tool does.
|
|
16
43
|
description="CLASP 3.0 / PEP 2606 static analysis tool. "
|
|
44
|
+
# Continue the description across multiple lines for readability.
|
|
17
45
|
"Checks variable, dict key, function, class naming "
|
|
46
|
+
# Complete the tool description with comment and log conventions.
|
|
18
47
|
"and comment/log conventions.",
|
|
48
|
+
# Close the ArgumentParser constructor call.
|
|
19
49
|
)
|
|
20
50
|
# Accept one or more file or directory paths as positional arguments.
|
|
21
51
|
parser_result.add_argument(
|
|
52
|
+
# Accept zero or more path strings as positional arguments.
|
|
22
53
|
"paths", nargs="*", default=["."],
|
|
54
|
+
# Provide the help text for the paths argument.
|
|
23
55
|
help="Python files or directories to check (default: current directory).",
|
|
56
|
+
# Close the first add_argument call.
|
|
24
57
|
)
|
|
25
58
|
# Provide a --version flag to display the tool version.
|
|
26
59
|
parser_result.add_argument(
|
|
60
|
+
# Use the version action to print the version string and exit.
|
|
27
61
|
"--version", action="version",
|
|
62
|
+
# Construct the version message from the package version.
|
|
28
63
|
version=f"CLASPLint {__version__}",
|
|
64
|
+
# Close the second add_argument call.
|
|
29
65
|
)
|
|
30
66
|
# Provide a flag to disable recursive directory traversal.
|
|
31
67
|
parser_result.add_argument(
|
|
68
|
+
# Store True when --no-recursive is passed on the command line.
|
|
32
69
|
"--no-recursive", action="store_true",
|
|
70
|
+
# Provide the help text for the no-recursive flag.
|
|
33
71
|
help="Do not recursively check subdirectories.",
|
|
72
|
+
# Close the third add_argument call.
|
|
34
73
|
)
|
|
35
74
|
# Provide a quiet mode that suppresses per-violation output.
|
|
36
75
|
parser_result.add_argument(
|
|
76
|
+
# Support both --quiet and -q flags for convenience.
|
|
37
77
|
"--quiet", "-q", action="store_true",
|
|
78
|
+
# Provide the help text for the quiet flag.
|
|
38
79
|
help="Suppress individual violation output; show only summary.",
|
|
80
|
+
# Close the fourth add_argument call.
|
|
39
81
|
)
|
|
40
82
|
# Provide a category filter to report only specific violation types.
|
|
41
83
|
parser_result.add_argument(
|
|
84
|
+
# Accept --category or -c with a choice of violation types.
|
|
42
85
|
"--category", "-c",
|
|
43
|
-
|
|
86
|
+
# Restrict the value to the five supported violation categories.
|
|
87
|
+
choices=["variable", "dict_key", "function", "comment", "log", "docstring"],
|
|
88
|
+
# Provide the help text for the category filter.
|
|
44
89
|
help="Only report violations of a specific category.",
|
|
90
|
+
# Close the fifth add_argument call.
|
|
45
91
|
)
|
|
46
92
|
# Return the fully configured argument parser.
|
|
47
93
|
return parser_result
|
|
@@ -61,6 +107,7 @@ def main(argv: list = None) -> int:
|
|
|
61
107
|
string_absolute = os.path.abspath(string_path)
|
|
62
108
|
# Only include paths that exist on the filesystem.
|
|
63
109
|
if os.path.exists(string_absolute):
|
|
110
|
+
# Add the resolved absolute path to the list.
|
|
64
111
|
list_resolved.append(string_absolute)
|
|
65
112
|
# Report an error if no valid paths were found.
|
|
66
113
|
if not list_resolved:
|
|
@@ -76,9 +123,11 @@ def main(argv: list = None) -> int:
|
|
|
76
123
|
if args.category:
|
|
77
124
|
# Filter violations to only the requested category.
|
|
78
125
|
report_result.list_violations = [
|
|
126
|
+
# Iterate over each violation to check against the filter.
|
|
79
127
|
v for v in report_result.list_violations
|
|
80
128
|
# Keep only violations matching the requested category filter.
|
|
81
129
|
if v.string_category == args.category
|
|
130
|
+
# Close the filtered list comprehension.
|
|
82
131
|
]
|
|
83
132
|
# Print the summary line to stdout.
|
|
84
133
|
print(report_result.summary())
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
THIS FILE IS PART OF CLASPLINT BY MATT BELFAST BROWN
|
|
3
|
+
CLASPLint.comment_checker — Validates that every code line has a preceding CLASP 3.0 formatted comment.
|
|
4
|
+
|
|
5
|
+
Author: Matt Belfast Brown
|
|
6
|
+
Create Date: 2026-06-17
|
|
7
|
+
Version Date: 2026-06-21
|
|
8
|
+
Version: 0.2.0
|
|
9
|
+
|
|
10
|
+
THIS PROGRAM IS LICENSED UNDER GPL-3.0
|
|
11
|
+
YOU SHOULD HAVE RECEIVED A COPY OF GPL-3.0 LICENSE.
|
|
12
|
+
|
|
13
|
+
Copyright (C) 2026 Matt Belfast Brown
|
|
14
|
+
|
|
15
|
+
This program is free software: you can redistribute it and/or modify
|
|
16
|
+
it under the terms of the GNU General Public License as published by
|
|
17
|
+
the Free Software Foundation, version 3 of the License.
|
|
18
|
+
|
|
19
|
+
This program is distributed in the hope that it will be useful,
|
|
20
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
21
|
+
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
22
|
+
GNU General Public License for more details.
|
|
23
|
+
|
|
24
|
+
You should have received a copy of the GNU General Public License
|
|
25
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
26
|
+
"""
|
|
2
27
|
|
|
3
28
|
import ast
|
|
4
29
|
import io
|
|
@@ -7,11 +32,18 @@ from typing import List, Set
|
|
|
7
32
|
|
|
8
33
|
from .reporter import Violation
|
|
9
34
|
|
|
10
|
-
# Define
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
35
|
+
# Define line-starting keywords that are exempt from the comment requirement.
|
|
36
|
+
# Imports, class definitions, and function definitions are structural declarations.
|
|
37
|
+
keywords_exemptfromcomment = frozenset({
|
|
38
|
+
# Include the standard import statement keyword.
|
|
39
|
+
"import",
|
|
40
|
+
# Include the from-import statement keyword.
|
|
41
|
+
"from",
|
|
42
|
+
# Include the class definition keyword.
|
|
43
|
+
"class",
|
|
44
|
+
# Include the function definition keyword.
|
|
45
|
+
"def",
|
|
46
|
+
# Close the exempt keywords frozenset literal.
|
|
15
47
|
})
|
|
16
48
|
|
|
17
49
|
|
|
@@ -22,9 +54,30 @@ def _tokenize_source(string_source: str) -> List[tokenize.TokenInfo]:
|
|
|
22
54
|
|
|
23
55
|
|
|
24
56
|
class CommentChecker:
|
|
25
|
-
"""
|
|
57
|
+
"""
|
|
58
|
+
Validates that every code line has a preceding comment and that all comments follow the CLASP 3.0 format.
|
|
59
|
+
|
|
60
|
+
Public methods:
|
|
61
|
+
run — Executes both the line-comment presence check and the comment format validation in sequence.
|
|
62
|
+
|
|
63
|
+
Private methods:
|
|
64
|
+
_init_line_comments_function_ — Verifies that every physical code line has a preceding comment per CLASP 3.0.
|
|
65
|
+
_init_comment_format_function_ — Validates that every comment follows the capitalized sentence format with a trailing period.
|
|
66
|
+
_init_docstring_line_function_ — Determines whether a given line number falls within a docstring expression.
|
|
67
|
+
"""
|
|
26
68
|
|
|
27
69
|
def __init__(self, string_filepath: str, string_source: str):
|
|
70
|
+
"""
|
|
71
|
+
Initialize the comment checker with the file path and full source code.
|
|
72
|
+
|
|
73
|
+
Stores the file path for violation reporting, the complete source as a string for
|
|
74
|
+
tokenization, and splits it into lines for per-line access by format checks.
|
|
75
|
+
|
|
76
|
+
:param string_filepath: The absolute path to the Python file being checked.
|
|
77
|
+
:type string_filepath: str
|
|
78
|
+
:param string_source: The complete source code of the file as a single string.
|
|
79
|
+
:type string_source: str
|
|
80
|
+
"""
|
|
28
81
|
# Store the file path for violation reporting.
|
|
29
82
|
self.string_filepath = string_filepath
|
|
30
83
|
# Store the full source code as a string.
|
|
@@ -35,16 +88,28 @@ class CommentChecker:
|
|
|
35
88
|
self.list_violations: List[Violation] = []
|
|
36
89
|
|
|
37
90
|
def run(self) -> None:
|
|
38
|
-
"""
|
|
91
|
+
"""
|
|
92
|
+
Run all comment format and presence checks.
|
|
93
|
+
|
|
94
|
+
Executes both the line-comment presence check and the comment format validation
|
|
95
|
+
in sequence, populating the internal violations list with any issues found.
|
|
96
|
+
"""
|
|
39
97
|
# Check that every code line requiring a comment has one.
|
|
40
98
|
self._init_line_comments_function_()
|
|
41
99
|
# Check that every comment follows the required format.
|
|
42
100
|
self._init_comment_format_function_()
|
|
43
101
|
|
|
44
102
|
def _init_line_comments_function_(self) -> None:
|
|
45
|
-
"""
|
|
103
|
+
"""
|
|
104
|
+
Verify that every physical code line has a preceding comment per CLASP 3.0.
|
|
105
|
+
|
|
106
|
+
Tokenizes the source code to identify code lines and comment lines, then checks
|
|
107
|
+
that every non-exempt code line has a comment on the same line or the line before.
|
|
108
|
+
Import, class, and function definition lines are exempt from this requirement.
|
|
109
|
+
"""
|
|
46
110
|
# Attempt to tokenize the source; skip if tokenization fails.
|
|
47
111
|
try:
|
|
112
|
+
# Tokenize the full source code into a list of token objects.
|
|
48
113
|
list_tokens = _tokenize_source(self.string_source)
|
|
49
114
|
# Return early if the source cannot be tokenized.
|
|
50
115
|
except tokenize.TokenError:
|
|
@@ -58,15 +123,21 @@ class CommentChecker:
|
|
|
58
123
|
for token_item in list_tokens:
|
|
59
124
|
# Add comment token line numbers to the comment set.
|
|
60
125
|
if token_item.type == tokenize.COMMENT:
|
|
126
|
+
# Record the line number of this comment token.
|
|
61
127
|
set_commentlines.add(token_item.start[0])
|
|
62
128
|
# Add single-line string token line numbers to the code set.
|
|
63
129
|
elif token_item.type == tokenize.STRING and token_item.start[0] == token_item.end[0]:
|
|
130
|
+
# Record the line number of this single-line string token.
|
|
64
131
|
set_codelines.add(token_item.start[0])
|
|
65
132
|
# Add all other meaningful token line numbers to the code set.
|
|
66
133
|
elif token_item.type not in (
|
|
134
|
+
# Exclude newline token types from the code line set.
|
|
67
135
|
tokenize.NEWLINE, tokenize.NL, tokenize.ENDMARKER,
|
|
136
|
+
# Exclude indentation token types from the code line set.
|
|
68
137
|
tokenize.INDENT, tokenize.DEDENT, tokenize.ENCODING,
|
|
138
|
+
# Close the excluded token type tuple.
|
|
69
139
|
):
|
|
140
|
+
# Record the line number of this meaningful code token.
|
|
70
141
|
set_codelines.add(token_item.start[0])
|
|
71
142
|
# Check each code line for required comments.
|
|
72
143
|
for int_lineno in sorted(set_codelines):
|
|
@@ -96,33 +167,52 @@ class CommentChecker:
|
|
|
96
167
|
string_previoustext = self.list_sourcelines[int_lineno - 2].strip()
|
|
97
168
|
# Treat a preceding comment line as satisfying the requirement.
|
|
98
169
|
if string_previoustext.startswith("#"):
|
|
170
|
+
# Mark this line as having a valid preceding comment.
|
|
99
171
|
bool_hascomment = True
|
|
100
|
-
# Report a violation if no comment was found for
|
|
172
|
+
# Report a violation if no comment was found for this code line.
|
|
101
173
|
if not bool_hascomment:
|
|
102
|
-
# Extract the first word
|
|
174
|
+
# Extract the first word to check against exempt structural keywords.
|
|
103
175
|
list_words = string_linetext.split()
|
|
104
176
|
# Default to empty string if the line has no words.
|
|
105
177
|
string_firstword = list_words[0] if list_words else ""
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
178
|
+
# Handle "async def" as a two-word function definition starter.
|
|
179
|
+
if string_firstword == "async" and len(list_words) >= 2:
|
|
180
|
+
# Extract the actual keyword following the async modifier.
|
|
181
|
+
string_firstword = list_words[1]
|
|
182
|
+
# Skip import, class, and function definition lines.
|
|
183
|
+
if string_firstword in keywords_exemptfromcomment:
|
|
184
|
+
# Proceed to the next line without reporting a violation.
|
|
185
|
+
continue
|
|
186
|
+
# Record a missing-comment violation for this physical code line.
|
|
187
|
+
self.list_violations.append(Violation(
|
|
188
|
+
# Supply the file path where the violation was detected.
|
|
189
|
+
string_filepath=self.string_filepath,
|
|
190
|
+
# Supply the line number of the uncommented code line.
|
|
191
|
+
int_linenumber=int_lineno,
|
|
192
|
+
# Supply the comment violation category identifier.
|
|
193
|
+
string_category="comment",
|
|
194
|
+
# Build the violation message with the line number.
|
|
195
|
+
string_message=(
|
|
196
|
+
# Format the line number into the missing-comment message.
|
|
197
|
+
f"Line {int_lineno} lacks a required preceding comment."
|
|
198
|
+
# Close the parenthesized message string.
|
|
199
|
+
),
|
|
200
|
+
# Supply the source line for contextual display.
|
|
201
|
+
string_sourceline=self.list_sourcelines[int_lineno - 1],
|
|
202
|
+
# Close the Violation data class instantiation.
|
|
203
|
+
))
|
|
121
204
|
|
|
122
205
|
def _init_comment_format_function_(self) -> None:
|
|
123
|
-
"""
|
|
206
|
+
"""
|
|
207
|
+
Verify that every comment follows the 'Capitalized sentence.' format.
|
|
208
|
+
|
|
209
|
+
Tokenizes the source and inspects each comment token for compliance with the
|
|
210
|
+
CLASP 3.0 comment format: the comment must start with '# ' (hash and space),
|
|
211
|
+
the text must begin with a capital letter, and it must end with a period.
|
|
212
|
+
"""
|
|
124
213
|
# Attempt to tokenize the source; skip if tokenization fails.
|
|
125
214
|
try:
|
|
215
|
+
# Tokenize the full source code for comment format checking.
|
|
126
216
|
list_tokens = _tokenize_source(self.string_source)
|
|
127
217
|
# Return early if the source cannot be tokenized.
|
|
128
218
|
except tokenize.TokenError:
|
|
@@ -140,14 +230,23 @@ class CommentChecker:
|
|
|
140
230
|
if not string_comment.startswith("# "):
|
|
141
231
|
# Record a violation for missing space after hash.
|
|
142
232
|
self.list_violations.append(Violation(
|
|
233
|
+
# Supply the file path where the malformed comment was detected.
|
|
143
234
|
string_filepath=self.string_filepath,
|
|
235
|
+
# Supply the line number of the malformed comment.
|
|
144
236
|
int_linenumber=token_item.start[0],
|
|
237
|
+
# Supply the comment violation category identifier.
|
|
145
238
|
string_category="comment",
|
|
239
|
+
# Build the violation message describing the format issue.
|
|
146
240
|
string_message=(
|
|
241
|
+
# Describe the required comment format.
|
|
147
242
|
f"Comment must start with '# ' (hash, space). "
|
|
243
|
+
# Show the first 20 characters of the malformed comment.
|
|
148
244
|
f"Found: '{string_comment[:20]}...'"
|
|
245
|
+
# Close the parenthesized message string.
|
|
149
246
|
),
|
|
247
|
+
# Supply the source line for contextual display.
|
|
150
248
|
string_sourceline=self.list_sourcelines[token_item.start[0] - 1],
|
|
249
|
+
# Close the Violation data class instantiation.
|
|
151
250
|
))
|
|
152
251
|
# Skip further checks on malformed comments.
|
|
153
252
|
continue
|
|
@@ -165,14 +264,23 @@ class CommentChecker:
|
|
|
165
264
|
if string_content[0].islower():
|
|
166
265
|
# Record a violation for lowercase start.
|
|
167
266
|
self.list_violations.append(Violation(
|
|
267
|
+
# Supply the file path where the malformed comment was detected.
|
|
168
268
|
string_filepath=self.string_filepath,
|
|
269
|
+
# Supply the line number of the malformed comment.
|
|
169
270
|
int_linenumber=token_item.start[0],
|
|
271
|
+
# Supply the comment violation category identifier.
|
|
170
272
|
string_category="comment",
|
|
273
|
+
# Build the violation message describing the format issue.
|
|
171
274
|
string_message=(
|
|
275
|
+
# Describe the required capitalization rule.
|
|
172
276
|
f"Comment text must start with a capital letter. "
|
|
277
|
+
# Show the first 30 characters of the malformed content.
|
|
173
278
|
f"Found: '{string_content[:30]}...'"
|
|
279
|
+
# Close the parenthesized message string.
|
|
174
280
|
),
|
|
281
|
+
# Supply the source line for contextual display.
|
|
175
282
|
string_sourceline=self.list_sourcelines[token_item.start[0] - 1],
|
|
283
|
+
# Close the Violation data class instantiation.
|
|
176
284
|
))
|
|
177
285
|
# Check that the comment text ends with a period or other valid punctuation.
|
|
178
286
|
if not string_content.endswith("."):
|
|
@@ -180,20 +288,41 @@ class CommentChecker:
|
|
|
180
288
|
if not any(string_content.endswith(p) for p in (".", "!", "?", ":", ";", ")")):
|
|
181
289
|
# Record a violation for missing terminal period.
|
|
182
290
|
self.list_violations.append(Violation(
|
|
291
|
+
# Supply the file path where the malformed comment was detected.
|
|
183
292
|
string_filepath=self.string_filepath,
|
|
293
|
+
# Supply the line number of the malformed comment.
|
|
184
294
|
int_linenumber=token_item.start[0],
|
|
295
|
+
# Supply the comment violation category identifier.
|
|
185
296
|
string_category="comment",
|
|
297
|
+
# Build the violation message describing the format issue.
|
|
186
298
|
string_message=(
|
|
299
|
+
# Describe the required trailing punctuation.
|
|
187
300
|
f"Comment must end with a period. "
|
|
301
|
+
# Show the first 40 characters of the malformed content.
|
|
188
302
|
f"Found: '{string_content[:40]}'"
|
|
303
|
+
# Close the parenthesized message string.
|
|
189
304
|
),
|
|
305
|
+
# Supply the source line for contextual display.
|
|
190
306
|
string_sourceline=self.list_sourcelines[token_item.start[0] - 1],
|
|
307
|
+
# Close the Violation data class instantiation.
|
|
191
308
|
))
|
|
192
309
|
|
|
193
310
|
def _init_docstring_line_function_(self, int_lineno: int) -> bool:
|
|
194
|
-
"""
|
|
311
|
+
"""
|
|
312
|
+
Determine whether a given line is part of a docstring.
|
|
313
|
+
|
|
314
|
+
Parses the source into an AST and walks all nodes to locate docstring expressions.
|
|
315
|
+
For each function, class, and module node with a docstring, checks whether the
|
|
316
|
+
queried line number falls within the docstring's start and end line range.
|
|
317
|
+
|
|
318
|
+
:param int_lineno: The 1-based line number to check for docstring membership.
|
|
319
|
+
:type int_lineno: int
|
|
320
|
+
:return: True if the line is within a docstring expression, False otherwise.
|
|
321
|
+
:rtype: bool
|
|
322
|
+
"""
|
|
195
323
|
# Attempt to parse the source into an AST; skip if parsing fails.
|
|
196
324
|
try:
|
|
325
|
+
# Parse the source code to locate docstring nodes.
|
|
197
326
|
tree = ast.parse(self.string_source)
|
|
198
327
|
# Return False if the source has syntax errors.
|
|
199
328
|
except SyntaxError:
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
THIS FILE IS PART OF CLASPLINT BY MATT BELFAST BROWN
|
|
3
|
+
CLASPLint.dict_key_checker — Validates dictionary key names against CLASP 3.0 PascalCase and abbreviation rules.
|
|
4
|
+
|
|
5
|
+
Author: Matt Belfast Brown
|
|
6
|
+
Create Date: 2026-06-17
|
|
7
|
+
Version Date: 2026-06-21
|
|
8
|
+
Version: 0.2.0
|
|
9
|
+
|
|
10
|
+
THIS PROGRAM IS LICENSED UNDER GPL-3.0
|
|
11
|
+
YOU SHOULD HAVE RECEIVED A COPY OF GPL-3.0 LICENSE.
|
|
12
|
+
|
|
13
|
+
Copyright (C) 2026 Matt Belfast Brown
|
|
14
|
+
|
|
15
|
+
This program is free software: you can redistribute it and/or modify
|
|
16
|
+
it under the terms of the GNU General Public License as published by
|
|
17
|
+
the Free Software Foundation, version 3 of the License.
|
|
18
|
+
|
|
19
|
+
This program is distributed in the hope that it will be useful,
|
|
20
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
21
|
+
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
22
|
+
GNU General Public License for more details.
|
|
23
|
+
|
|
24
|
+
You should have received a copy of the GNU General Public License
|
|
25
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import ast
|
|
29
|
+
from typing import List
|
|
30
|
+
|
|
31
|
+
from .naming_utils import validate_dictkey_format
|
|
32
|
+
from .reporter import Violation
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DictKeyChecker(ast.NodeVisitor):
|
|
36
|
+
"""
|
|
37
|
+
Walks the AST and validates dictionary key names against CLASP 3.0 PascalCase naming rules.
|
|
38
|
+
|
|
39
|
+
Public methods:
|
|
40
|
+
visit_Dict — Checks each key in dictionary literal expressions against PascalCase format.
|
|
41
|
+
visit_Call — Checks keyword argument names in dict() constructor calls.
|
|
42
|
+
visit_Subscript — Visits subscript nodes for potential dictionary key access in assignments.
|
|
43
|
+
|
|
44
|
+
Private methods:
|
|
45
|
+
_init_key_function_ — Validates a single string-constant dictionary key against CLASP 3.0 rules.
|
|
46
|
+
_init_keyword_as_key_function_ — Validates a keyword argument name used as a dictionary key identifier.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, string_filepath: str, list_sourcelines: List[str]):
|
|
50
|
+
"""Initialize the dict key checker with file path and source lines for context.
|
|
51
|
+
|
|
52
|
+
Stores the provided file path and source lines for later use in violation
|
|
53
|
+
reporting and contextual display. Also initializes an empty list to collect
|
|
54
|
+
violations discovered during AST traversal.
|
|
55
|
+
|
|
56
|
+
:param string_filepath: Path to the file being checked.
|
|
57
|
+
:type string_filepath: str
|
|
58
|
+
:param list_sourcelines: Source lines of the file being checked.
|
|
59
|
+
:type list_sourcelines: List[str]
|
|
60
|
+
"""
|
|
61
|
+
# Store the file path for violation reporting.
|
|
62
|
+
self.string_filepath = string_filepath
|
|
63
|
+
# Store source lines for contextual violation messages.
|
|
64
|
+
self.list_sourcelines = list_sourcelines
|
|
65
|
+
# Collect violations found during AST traversal.
|
|
66
|
+
self.list_violations: List[Violation] = []
|
|
67
|
+
|
|
68
|
+
def _init_key_function_(self, node_key: ast.AST, node_dict: ast.AST) -> None:
|
|
69
|
+
"""Validate a single dictionary key literal.
|
|
70
|
+
|
|
71
|
+
Verifies that the key is a string constant before applying CLASP 3.0 dict key
|
|
72
|
+
format validation. Each detected format violation is appended to the violations
|
|
73
|
+
list with its source line context.
|
|
74
|
+
|
|
75
|
+
:param node_key: The dictionary key AST node to validate.
|
|
76
|
+
:type node_key: ast.AST
|
|
77
|
+
:param node_dict: The parent dictionary AST node.
|
|
78
|
+
:type node_dict: ast.AST
|
|
79
|
+
"""
|
|
80
|
+
# Only string literal keys are checked; skip non-constant or non-string keys.
|
|
81
|
+
if not isinstance(node_key, ast.Constant):
|
|
82
|
+
# Exit early for non-constant dict key nodes.
|
|
83
|
+
return
|
|
84
|
+
# Skip keys whose values are not strings.
|
|
85
|
+
if not isinstance(node_key.value, str):
|
|
86
|
+
# Exit early for keys whose values are not strings.
|
|
87
|
+
return
|
|
88
|
+
# Extract the key string value.
|
|
89
|
+
string_key = node_key.value
|
|
90
|
+
# Run the CLASP 3.0 dict key format validation.
|
|
91
|
+
list_issues = validate_dictkey_format(string_key)
|
|
92
|
+
# Retrieve the source line for contextual output.
|
|
93
|
+
string_sourceline = ""
|
|
94
|
+
# Ensure the line number is within bounds before accessing source lines.
|
|
95
|
+
if node_key.lineno and node_key.lineno <= len(self.list_sourcelines):
|
|
96
|
+
# Assign the source line at the detected line number.
|
|
97
|
+
string_sourceline = self.list_sourcelines[node_key.lineno - 1]
|
|
98
|
+
# Append each detected violation to the violations list.
|
|
99
|
+
for string_message in list_issues:
|
|
100
|
+
# Construct a Violation with full file and line context.
|
|
101
|
+
self.list_violations.append(Violation(
|
|
102
|
+
# Supply the file path where the violation was detected.
|
|
103
|
+
string_filepath=self.string_filepath,
|
|
104
|
+
# Supply the line number of the offending dictionary key.
|
|
105
|
+
int_linenumber=node_key.lineno,
|
|
106
|
+
# Supply the violation category identifier.
|
|
107
|
+
string_category="dict_key",
|
|
108
|
+
# Supply the human-readable violation message.
|
|
109
|
+
string_message=string_message,
|
|
110
|
+
# Supply the source line for contextual display.
|
|
111
|
+
string_sourceline=string_sourceline,
|
|
112
|
+
# Close the Violation data class instantiation.
|
|
113
|
+
))
|
|
114
|
+
|
|
115
|
+
def visit_Dict(self, node: ast.Dict) -> None:
|
|
116
|
+
"""Check keys in dictionary literal expressions.
|
|
117
|
+
|
|
118
|
+
Iterates over every key node in the dictionary literal and delegates
|
|
119
|
+
non-None keys to the key validation method. Then continues generic
|
|
120
|
+
visitation of child nodes to handle nested dictionary literals.
|
|
121
|
+
|
|
122
|
+
:param node: The dictionary AST node to visit.
|
|
123
|
+
:type node: ast.Dict
|
|
124
|
+
"""
|
|
125
|
+
# Iterate over each key node in the dictionary literal.
|
|
126
|
+
for node_key in node.keys:
|
|
127
|
+
# Process non-None keys through the key validator.
|
|
128
|
+
if node_key is not None:
|
|
129
|
+
# Delegate to the key validation method for this dictionary key.
|
|
130
|
+
self._init_key_function_(node_key, node)
|
|
131
|
+
# Continue visiting child nodes for nested dictionaries.
|
|
132
|
+
self.generic_visit(node)
|
|
133
|
+
|
|
134
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
135
|
+
"""Check dict() constructor calls for keyword argument key names.
|
|
136
|
+
|
|
137
|
+
Detects calls to the built-in dict() constructor and validates each keyword
|
|
138
|
+
argument name against CLASP 3.0 dict key naming rules. Non-dict calls are
|
|
139
|
+
skipped and generic visitation continues for nested structures.
|
|
140
|
+
|
|
141
|
+
:param node: The call AST node to visit.
|
|
142
|
+
:type node: ast.Call
|
|
143
|
+
"""
|
|
144
|
+
# Detect calls to the built-in dict() constructor.
|
|
145
|
+
if isinstance(node.func, ast.Name) and node.func.id == "dict":
|
|
146
|
+
# Check each keyword argument name as a potential dict key.
|
|
147
|
+
for node_keyword in node.keywords:
|
|
148
|
+
# Validate the keyword argument if it has an explicit name.
|
|
149
|
+
if node_keyword.arg:
|
|
150
|
+
# Delegate to the keyword-as-key validation method.
|
|
151
|
+
self._init_keyword_as_key_function_(node_keyword)
|
|
152
|
+
# Continue visiting child nodes.
|
|
153
|
+
self.generic_visit(node)
|
|
154
|
+
|
|
155
|
+
def _init_keyword_as_key_function_(self, node_keyword: ast.keyword) -> None:
|
|
156
|
+
"""Validate a keyword argument name used as a dictionary key.
|
|
157
|
+
|
|
158
|
+
Extracts the keyword argument name from the AST node and validates it against
|
|
159
|
+
CLASP 3.0 rules. Currently only flags empty keyword names, as Python keyword
|
|
160
|
+
arguments are identifiers and cannot directly use PascalCase.
|
|
161
|
+
|
|
162
|
+
:param node_keyword: The keyword AST node to validate.
|
|
163
|
+
:type node_keyword: ast.keyword
|
|
164
|
+
"""
|
|
165
|
+
# Extract the keyword argument name.
|
|
166
|
+
string_key = node_keyword.arg
|
|
167
|
+
# Skip empty or missing keyword argument names.
|
|
168
|
+
if not string_key:
|
|
169
|
+
# Exit early for empty keyword argument names.
|
|
170
|
+
return
|
|
171
|
+
# Python keyword arguments are identifiers and cannot use PascalCase directly;
|
|
172
|
+
# Only flag if the name clearly violates PascalCase convention expectations.
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
def visit_Subscript(self, node: ast.Subscript) -> None:
|
|
176
|
+
"""Visit subscript nodes for potential dict key access within Assign statements.
|
|
177
|
+
|
|
178
|
+
Subscript key checks are handled during Assign target walking rather than at
|
|
179
|
+
the individual subscript node level. This method simply continues generic
|
|
180
|
+
visitation of child nodes for completeness.
|
|
181
|
+
|
|
182
|
+
:param node: The subscript AST node to visit.
|
|
183
|
+
:type node: ast.Subscript
|
|
184
|
+
"""
|
|
185
|
+
# Subscript key checks are handled during Assign target walking.
|
|
186
|
+
self.generic_visit(node)
|