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.
Files changed (29) hide show
  1. CLASPLint-0.2.0/CLASPLint/__init__.py +34 -0
  2. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint/__main__.py +51 -2
  3. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint/comment_checker.py +157 -28
  4. CLASPLint-0.2.0/CLASPLint/dict_key_checker.py +186 -0
  5. CLASPLint-0.2.0/CLASPLint/docstring_checker.py +436 -0
  6. CLASPLint-0.2.0/CLASPLint/function_checker.py +265 -0
  7. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint/log_checker.py +158 -9
  8. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint/naming_utils.py +149 -6
  9. CLASPLint-0.2.0/CLASPLint/reporter.py +186 -0
  10. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint/runner.py +54 -1
  11. CLASPLint-0.2.0/CLASPLint/variable_checker.py +339 -0
  12. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint.egg-info/PKG-INFO +16 -13
  13. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint.egg-info/SOURCES.txt +2 -0
  14. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/LICENSE +4 -3
  15. CLASPLint-0.2.0/MANIFEST.in +1 -0
  16. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/PKG-INFO +16 -13
  17. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/README.md +14 -10
  18. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/pyproject.toml +2 -3
  19. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/setup.py +1 -2
  20. CLASPLint-0.1.0/CLASPLint/__init__.py +0 -6
  21. CLASPLint-0.1.0/CLASPLint/dict_key_checker.py +0 -87
  22. CLASPLint-0.1.0/CLASPLint/function_checker.py +0 -95
  23. CLASPLint-0.1.0/CLASPLint/reporter.py +0 -90
  24. CLASPLint-0.1.0/CLASPLint/variable_checker.py +0 -159
  25. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint.egg-info/dependency_links.txt +0 -0
  26. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint.egg-info/entry_points.txt +0 -0
  27. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint.egg-info/top_level.txt +0 -0
  28. {CLASPLint-0.1.0 → CLASPLint-0.2.0}/CLASPLint.egg-info/zip-safe +0 -0
  29. {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
- # Provide the CLASPLint command-line interface entry point.
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
- choices=["variable", "dict_key", "function", "comment", "log"],
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
- # Check that every code line has a proper CLASP 3.0 formatted comment.
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 control flow and operation keywords that require a preceding comment.
11
- keywords_requirecomment = frozenset({
12
- "if", "elif", "else", "for", "while", "try", "except",
13
- "finally", "with", "return", "raise", "continue", "break",
14
- "pass",
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
- """Check that every code line has a proper CLASP 3.0 comment."""
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
- """Run all comment format and presence checks."""
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
- """Verify that every physical code line with a control keyword has a comment."""
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 a keyword line.
172
+ # Report a violation if no comment was found for this code line.
101
173
  if not bool_hascomment:
102
- # Extract the first word of the line to check against required keywords.
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
- # Strip trailing colon from keywords like "if:" or "else:".
107
- string_keyword = string_firstword.rstrip(":")
108
- # Check if this line's keyword requires a comment.
109
- if string_keyword in keywords_requirecomment:
110
- # Record a missing-comment violation for this keyword line.
111
- self.list_violations.append(Violation(
112
- string_filepath=self.string_filepath,
113
- int_linenumber=int_lineno,
114
- string_category="comment",
115
- string_message=(
116
- f"Line {int_lineno} with keyword '{string_keyword}' "
117
- f"lacks a required preceding comment."
118
- ),
119
- string_sourceline=self.list_sourcelines[int_lineno - 1],
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
- """Verify that every comment follows the 'Capitalized sentence.' format."""
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
- """Determine whether a given line is part of a docstring."""
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)