lizard 1.18.0__py2.py3-none-any.whl → 1.20.0__py2.py3-none-any.whl

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 (40) hide show
  1. {lizard-1.18.0.dist-info → lizard-1.20.0.dist-info}/METADATA +30 -20
  2. lizard-1.20.0.dist-info/RECORD +70 -0
  3. lizard_ext/__init__.py +0 -1
  4. lizard_ext/htmloutput.py +86 -17
  5. lizard_ext/lizardcomplextags.py +6 -0
  6. lizard_ext/lizardmccabe.py +6 -1
  7. lizard_ext/lizardmodified.py +7 -2
  8. lizard_ext/lizardnd.py +64 -1
  9. lizard_ext/lizardnonstrict.py +2 -1
  10. lizard_ext/lizardns.py +31 -8
  11. lizard_ext/version.py +1 -1
  12. lizard_languages/__init__.py +2 -0
  13. lizard_languages/clike.py +6 -3
  14. lizard_languages/code_reader.py +27 -3
  15. lizard_languages/csharp.py +21 -2
  16. lizard_languages/erlang.py +9 -1
  17. lizard_languages/fortran.py +5 -6
  18. lizard_languages/gdscript.py +6 -2
  19. lizard_languages/kotlin.py +6 -3
  20. lizard_languages/perl.py +8 -4
  21. lizard_languages/php.py +6 -2
  22. lizard_languages/plsql.py +422 -0
  23. lizard_languages/python.py +6 -4
  24. lizard_languages/r.py +9 -6
  25. lizard_languages/rubylike.py +6 -3
  26. lizard_languages/rust.py +7 -2
  27. lizard_languages/scala.py +6 -2
  28. lizard_languages/solidity.py +6 -1
  29. lizard_languages/st.py +9 -5
  30. lizard_languages/swift.py +6 -2
  31. lizard_languages/tnsdl.py +6 -1
  32. lizard_languages/tsx.py +2 -2
  33. lizard_languages/ttcn.py +5 -3
  34. lizard_languages/typescript.py +58 -4
  35. lizard_languages/zig.py +7 -1
  36. lizard-1.18.0.dist-info/RECORD +0 -69
  37. {lizard-1.18.0.dist-info → lizard-1.20.0.dist-info}/LICENSE.txt +0 -0
  38. {lizard-1.18.0.dist-info → lizard-1.20.0.dist-info}/WHEEL +0 -0
  39. {lizard-1.18.0.dist-info → lizard-1.20.0.dist-info}/entry_points.txt +0 -0
  40. {lizard-1.18.0.dist-info → lizard-1.20.0.dist-info}/top_level.txt +0 -0
@@ -11,8 +11,11 @@ class CSharpReader(CLikeReader):
11
11
  ext = ['cs']
12
12
  language_names = ['csharp']
13
13
 
14
- _conditions = set(['if', 'for', 'while', '&&', '||', '?', 'catch',
15
- 'case', '??'])
14
+ # Separated condition categories
15
+ _control_flow_keywords = {'if', 'for', 'while', 'catch'}
16
+ _logical_operators = {'&&', '||'}
17
+ _case_keywords = {'case'}
18
+ _ternary_operators = {'?', '??'} # C# has both ?: and ?? (null-coalescing)
16
19
 
17
20
  def __init__(self, context):
18
21
  super(CSharpReader, self).__init__(context)
@@ -39,6 +42,22 @@ class CSharpStates(CLikeStates):
39
42
  if self.class_name and self.context.current_function:
40
43
  self.context.current_function.name = f"{self.class_name}::{name}"
41
44
 
45
+ def _state_dec_to_imp(self, token):
46
+ """Override to handle C# expression-bodied members (=>)"""
47
+ if token == '=>':
48
+ # Expression-bodied member: confirm function and enter expression body
49
+ self.context.confirm_new_function()
50
+ self._state = self._state_expression_body
51
+ else:
52
+ super(CSharpStates, self)._state_dec_to_imp(token)
53
+
54
+ def _state_expression_body(self, token):
55
+ """Read the expression body until semicolon, processing tokens for complexity"""
56
+ if token == ';':
57
+ # End of expression-bodied method, finalize function and return to global state
58
+ self.context.end_of_function()
59
+ self._state = self._state_global
60
+
42
61
  def _state_global(self, token):
43
62
  if token in ("class", "struct", "record"):
44
63
  self.class_name = None
@@ -13,7 +13,15 @@ class ErlangReader(CodeReader):
13
13
 
14
14
  ext = ['erl', 'hrl', 'es', 'escript']
15
15
  language_names = ['erlang']
16
- _conditions = {'and', 'case', 'catch', 'if', 'not', 'or', '?', 'when'}
16
+
17
+ # Separated condition categories
18
+ _control_flow_keywords = {'if', 'catch', 'when'} # when is used in guards
19
+ _logical_operators = {'and', 'or', 'not'}
20
+ _case_keywords = {'case'}
21
+ # Note: '?' in Erlang is a macro expansion operator (e.g., ?MODULE, ?EMPTY_NODE)
22
+ # Unlike C-style ternary, it's for compile-time macro substitution
23
+ # Included in ternary_operators because macro usage adds to code complexity
24
+ _ternary_operators = {'?'}
17
25
 
18
26
  def __init__(self, context):
19
27
  super(ErlangReader, self).__init__(context)
@@ -19,12 +19,11 @@ class FortranReader(CodeReader, FortranCommentsMixin):
19
19
  ext = ['f70', 'f90', 'f95', 'f03', 'f08', 'f', 'for', 'ftn', 'fpp']
20
20
  language_names = ['fortran']
21
21
 
22
- # Conditions need to have all the cases because the matching is case-insensitive
23
- # and is not done here.
24
- _conditions = {
25
- 'IF', 'DO', '.AND.', '.OR.', 'CASE',
26
- 'if', 'do', '.and.', '.or.', 'case'
27
- }
22
+ # Separated condition categories (case-insensitive language)
23
+ _control_flow_keywords = {'IF', 'DO', 'if', 'do'}
24
+ _logical_operators = {'.AND.', '.OR.', '.and.', '.or.'}
25
+ _case_keywords = {'CASE', 'case'}
26
+ _ternary_operators = set()
28
27
  _blocks = [
29
28
  'PROGRAM', 'MODULE', 'SUBMODULE', 'SUBROUTINE', 'FUNCTION', 'TYPE',
30
29
  'INTERFACE', 'BLOCK', 'IF', 'DO', 'FORALL', 'WHERE', 'SELECT', 'ASSOCIATE'
@@ -9,8 +9,12 @@ class GDScriptReader(PythonReader):
9
9
 
10
10
  ext = ['gd']
11
11
  language_names = ['GDScript']
12
- _conditions = set(['if', 'else', 'for', 'while', '&&', '||', '?', 'catch',
13
- 'case', 'do'])
12
+
13
+ # Separated condition categories
14
+ _control_flow_keywords = {'if', 'elif', 'for', 'while', 'catch', 'do'}
15
+ _logical_operators = {'&&', '||'}
16
+ _case_keywords = {'case'}
17
+ _ternary_operators = {'?'}
14
18
 
15
19
  def __init__(self, context):
16
20
  super(GDScriptReader, self).__init__(context)
@@ -13,9 +13,12 @@ class KotlinReader(CodeReader, CCppCommentsMixin, SwiftReplaceLabel):
13
13
 
14
14
  ext = ['kt', 'kts']
15
15
  language_names = ['kotlin']
16
- _conditions = {
17
- 'if', 'for', 'while', 'catch', '&&', '||', '?:'
18
- }
16
+
17
+ # Separated condition categories
18
+ _control_flow_keywords = {'if', 'for', 'while', 'catch'}
19
+ _logical_operators = {'&&', '||'}
20
+ _case_keywords = set() # Kotlin uses 'when' expressions, not case
21
+ _ternary_operators = {'?:'} # Elvis operator
19
22
 
20
23
  def __init__(self, context):
21
24
  super(KotlinReader, self).__init__(context)
lizard_languages/perl.py CHANGED
@@ -19,8 +19,13 @@ class PerlReader(CodeReader, ScriptLanguageMixIn):
19
19
 
20
20
  ext = ['pl', 'pm']
21
21
  language_names = ['perl']
22
- _conditions = set(['if', 'elsif', 'unless', 'while', 'until', 'for', 'foreach',
23
- '&&', '||', '?', ':', 'when', 'given', 'default', 'do'])
22
+
23
+ # Separated condition categories
24
+ _control_flow_keywords = {'if', 'elsif', 'unless', 'while', 'until', 'for',
25
+ 'foreach', 'when', 'given', 'default', 'do'}
26
+ _logical_operators = {'&&', '||'} # Perl also has 'and', 'or' with different precedence
27
+ _case_keywords = set()
28
+ _ternary_operators = {'?', ':'} # Both parts of ternary operator
24
29
 
25
30
  def __init__(self, context):
26
31
  super(PerlReader, self).__init__(context)
@@ -65,8 +70,7 @@ class PerlReader(CodeReader, ScriptLanguageMixIn):
65
70
 
66
71
 
67
72
  class PerlStates(CodeStateMachine):
68
- _conditions = set(['if', 'elsif', 'unless', 'while', 'until', 'for', 'foreach',
69
- '&&', '||', '?', ':', 'when', 'given', 'default', 'do'])
73
+ # Note: _conditions removed - now inherited from PerlReader
70
74
 
71
75
  def __init__(self, context):
72
76
  super(PerlStates, self).__init__(context)
lizard_languages/php.py CHANGED
@@ -231,8 +231,12 @@ class PHPReader(CodeReader, CCppCommentsMixin):
231
231
 
232
232
  ext = ['php']
233
233
  language_names = ['php']
234
- _conditions = set(['if', 'elseif', 'for', 'foreach', 'while', '&&', '||', '?',
235
- 'catch', 'case', 'match'])
234
+
235
+ # Separated condition categories
236
+ _control_flow_keywords = {'if', 'elseif', 'for', 'foreach', 'while', 'catch', 'match'}
237
+ _logical_operators = {'&&', '||'} # PHP also has 'and', 'or' with different precedence
238
+ _case_keywords = {'case'}
239
+ _ternary_operators = {'?'}
236
240
 
237
241
  @staticmethod
238
242
  def generate_tokens(source_code, addition='', token_class=None):
@@ -0,0 +1,422 @@
1
+ """
2
+ Language parser for PL/SQL (Oracle's Procedural Language extension to SQL)
3
+
4
+ This module implements complexity analysis for PL/SQL code, supporting:
5
+ - Procedures, Functions, and Triggers
6
+ - Package Bodies (not specifications - they only contain signatures)
7
+ - Nested procedures and functions
8
+ - Anonymous blocks with nested functions (blocks themselves aren't counted)
9
+ - Control structures: IF/ELSIF/ELSE, CASE/WHEN, LOOP/WHILE/FOR
10
+ - Exception handlers
11
+ - Cursor declarations and cursor FOR loops
12
+
13
+ Design Decisions:
14
+ - EXIT WHEN: The WHEN keyword is filtered out by the preprocessor because
15
+ "EXIT WHEN condition" is not a branching construct - it's a conditional
16
+ exit that doesn't create alternate execution paths. The LOOP itself adds
17
+ complexity.
18
+
19
+ - CONTINUE WHEN: Similar to EXIT WHEN, the WHEN is counted as it does create
20
+ a branch in the loop execution.
21
+
22
+ - GOTO: Does not add to cyclomatic complexity as it's just an unconditional
23
+ jump, not a decision point.
24
+
25
+ - Standalone LOOP: Adds +1 complexity as it creates a repeating path.
26
+
27
+ - FOR/WHILE LOOP: The FOR/WHILE keyword adds complexity; the following LOOP
28
+ keyword is part of the same construct and doesn't add additional complexity.
29
+
30
+ - Parameter Counting: Currently counts all non-whitespace tokens and commas
31
+ in parameter lists. This approach works but differs from some other language
32
+ implementations.
33
+ """
34
+
35
+ from .code_reader import CodeReader, CodeStateMachine
36
+ from .clike import CCppCommentsMixin
37
+
38
+
39
+ class PLSQLReader(CodeReader, CCppCommentsMixin):
40
+ """
41
+ Reader for PL/SQL language supporting procedures, functions, packages,
42
+ and core control structures.
43
+ """
44
+
45
+ ext = ["sql", "pks", "pkb", "pls", "plb", "pck"]
46
+ language_names = ["plsql", "pl/sql"]
47
+
48
+ # Separated condition categories
49
+ # Note: 'loop' is NOT in this set because LOOP has special handling:
50
+ # - standalone LOOP adds +1
51
+ # - LOOP after WHILE/FOR should not add (it's part of the compound statement)
52
+ _control_flow_keywords = {"if", "elsif", "when", "while", "for"}
53
+ _logical_operators = {"and", "or"}
54
+ _case_keywords = set() # PL/SQL uses 'when' in case expressions
55
+ _ternary_operators = set()
56
+
57
+ def __init__(self, context):
58
+ super(PLSQLReader, self).__init__(context)
59
+ self.parallel_states = [PLSQLStates(context)]
60
+ # PL/SQL is case-insensitive, so add both lowercase and uppercase versions
61
+ # of keywords to the conditions set for the automatic condition counter
62
+ self.conditions = self.conditions | {c.upper() for c in self.conditions}
63
+
64
+ def preprocess(self, tokens):
65
+ """
66
+ Preprocess tokens to handle PL/SQL-specific constructs.
67
+ Merge compound keywords to prevent the condition counter from double-counting:
68
+ - "END IF", "END LOOP", "END CASE", "END WHILE", "END FOR" -> single tokens
69
+ - "EXIT WHEN" -> remove the WHEN keyword (EXIT doesn't create a branch)
70
+ """
71
+ last_nonwhitespace_token = None
72
+ pending_tokens = []
73
+
74
+ for token in tokens:
75
+ if not token.isspace() or token == "\n":
76
+ token_upper = token.upper()
77
+
78
+ # Handle "END IF", "END LOOP", etc.
79
+ if (
80
+ last_nonwhitespace_token
81
+ and last_nonwhitespace_token.upper() == "END"
82
+ ):
83
+ if token_upper in ("IF", "LOOP", "CASE", "WHILE", "FOR"):
84
+ # Merge into "END_IF", "END_LOOP", etc.
85
+ yield "END_" + token_upper
86
+ last_nonwhitespace_token = None
87
+ pending_tokens = []
88
+ continue
89
+
90
+ # Handle "EXIT WHEN" - skip the WHEN keyword
91
+ if (
92
+ last_nonwhitespace_token
93
+ and last_nonwhitespace_token.upper() == "EXIT"
94
+ and token_upper == "WHEN"
95
+ ):
96
+ # Skip this WHEN keyword
97
+ pending_tokens = []
98
+ continue
99
+
100
+ # Yield any pending tokens
101
+ if last_nonwhitespace_token:
102
+ yield last_nonwhitespace_token
103
+ for pending in pending_tokens:
104
+ yield pending
105
+ pending_tokens = []
106
+
107
+ # Update tracking
108
+ last_nonwhitespace_token = token
109
+ else:
110
+ # Accumulate whitespace
111
+ pending_tokens.append(token)
112
+
113
+ # Don't forget the last tokens
114
+ if last_nonwhitespace_token:
115
+ yield last_nonwhitespace_token
116
+ for pending in pending_tokens:
117
+ yield pending
118
+
119
+ @staticmethod
120
+ def generate_tokens(source_code, addition="", token_class=None):
121
+ """
122
+ Generate tokens for PL/SQL code.
123
+ PL/SQL uses:
124
+ - Single-line comments: --
125
+ - Multi-line comments: /* */
126
+ - String literals: 'text' (with '' for escaping)
127
+ - Assignment operator: :=
128
+ """
129
+ # Add PL/SQL-specific patterns
130
+ addition = r"|--[^\n]*" + addition # Single-line comment starting with --
131
+ return CodeReader.generate_tokens(source_code, addition, token_class)
132
+
133
+ def get_comment_from_token(self, token):
134
+ """
135
+ Override to recognize PL/SQL's -- line comments in addition to /* */ block comments.
136
+ PL/SQL uses -- for single-line comments (like SQL standard).
137
+
138
+ Note: This method correctly identifies -- comments, but due to a limitation in
139
+ the NLOC calculation, these comments may still be counted in NLOC.
140
+ """
141
+ if token.startswith("--"):
142
+ return token # Return full comment token (like Lua does)
143
+ # Delegate to parent for /* */ and // comments
144
+ return super().get_comment_from_token(token)
145
+
146
+
147
+ class PLSQLStates(CodeStateMachine):
148
+ """
149
+ State machine for parsing PL/SQL code structure.
150
+ """
151
+
152
+ def __init__(self, context):
153
+ super(PLSQLStates, self).__init__(context)
154
+ self.in_parameter_list = False
155
+ self.last_control_keyword = None # Track FOR/WHILE to avoid counting their LOOP
156
+ self.declaring_nested_function = (
157
+ False # Track if we're declaring a nested function
158
+ )
159
+
160
+ def _state_global(self, token):
161
+ """Global state - looking for function/procedure/trigger declarations."""
162
+ token_lower = token.lower()
163
+
164
+ if token_lower == "procedure":
165
+ self.next(self._procedure_name)
166
+ elif token_lower == "function":
167
+ self.next(self._function_name)
168
+ elif token_lower == "trigger":
169
+ self.next(self._trigger_name)
170
+
171
+ def _procedure_name(self, token):
172
+ """Read procedure name."""
173
+ if token.isspace() or token == "\n":
174
+ return
175
+ if token == "(":
176
+ self.in_parameter_list = True
177
+ self.next(self._parameters, "(")
178
+ elif token.lower() in ("is", "as"):
179
+ self.context.confirm_new_function()
180
+ self.next(self._state_before_begin)
181
+ else:
182
+ # Check if this is a nested function
183
+ if self.declaring_nested_function:
184
+ self.context.push_new_function(token)
185
+ self.declaring_nested_function = False
186
+ else:
187
+ self.context.try_new_function(token)
188
+ self.next(self._procedure_after_name)
189
+
190
+ def _procedure_after_name(self, token):
191
+ """After procedure name, look for parameters or IS/AS."""
192
+ if token == ".":
193
+ # Schema-qualified name: the previous token was the schema,
194
+ # next non-whitespace token will be the actual procedure name
195
+ self.next(self._procedure_name_after_dot)
196
+ elif token == "(":
197
+ self.in_parameter_list = True
198
+ self.next(self._parameters, "(")
199
+ elif token.lower() in ("is", "as"):
200
+ self.context.confirm_new_function()
201
+ self.next(self._state_before_begin)
202
+ # Skip whitespace and other tokens
203
+
204
+ def _procedure_name_after_dot(self, token):
205
+ """Read the actual procedure name after schema.dot."""
206
+ if token.isspace() or token == "\n":
207
+ return
208
+ # Replace the previous (schema) name with the actual procedure name
209
+ self.context.current_function.name = token
210
+ self.next(self._procedure_after_name)
211
+
212
+ def _function_name(self, token):
213
+ """Read function name."""
214
+ if token.isspace() or token == "\n":
215
+ return
216
+ if token == "(":
217
+ self.in_parameter_list = True
218
+ self.next(self._parameters, "(")
219
+ elif token.lower() == "return":
220
+ self.next(self._return_type)
221
+ elif token.lower() in ("is", "as"):
222
+ self.context.confirm_new_function()
223
+ self.next(self._state_before_begin)
224
+ else:
225
+ # Check if this is a nested function
226
+ if self.declaring_nested_function:
227
+ self.context.push_new_function(token)
228
+ self.declaring_nested_function = False
229
+ else:
230
+ self.context.try_new_function(token)
231
+ self.next(self._function_after_name)
232
+
233
+ def _function_after_name(self, token):
234
+ """After function name, look for parameters, RETURN, or IS/AS."""
235
+ if token == ".":
236
+ # Schema-qualified name: the previous token was the schema,
237
+ # next non-whitespace token will be the actual function name
238
+ self.next(self._function_name_after_dot)
239
+ elif token == "(":
240
+ self.in_parameter_list = True
241
+ self.next(self._parameters, "(")
242
+ elif token.lower() == "return":
243
+ self.next(self._return_type)
244
+ elif token.lower() in ("is", "as"):
245
+ self.context.confirm_new_function()
246
+ self.next(self._state_before_begin)
247
+ # Skip whitespace and other tokens
248
+
249
+ def _function_name_after_dot(self, token):
250
+ """Read the actual function name after schema.dot."""
251
+ if token.isspace() or token == "\n":
252
+ return
253
+ # Replace the previous (schema) name with the actual function name
254
+ self.context.current_function.name = token
255
+ self.next(self._function_after_name)
256
+
257
+ def _return_type(self, token):
258
+ """Skip return type declaration."""
259
+ if token.lower() in ("is", "as"):
260
+ self.context.confirm_new_function()
261
+ self.next(self._state_before_begin)
262
+ # Skip everything else (return type tokens)
263
+
264
+ def _parameters(self, token):
265
+ """Read parameters."""
266
+ if token == ")":
267
+ self.in_parameter_list = False
268
+ self.next(self._after_parameters)
269
+ elif token == ",":
270
+ # Each comma separates parameters
271
+ self.context.parameter(token)
272
+ elif not token.isspace() and token != "\n":
273
+ # Track non-whitespace tokens as potential parameters
274
+ self.context.parameter(token)
275
+
276
+ def _after_parameters(self, token):
277
+ """After parameters, look for IS/AS or RETURN."""
278
+ if token.lower() == "return":
279
+ self.next(self._return_type)
280
+ elif token.lower() in ("is", "as"):
281
+ self.context.confirm_new_function()
282
+ self.next(self._state_before_begin)
283
+ # Skip whitespace and other tokens
284
+
285
+ def _trigger_name(self, token):
286
+ """Read trigger name."""
287
+ if token.isspace() or token == "\n":
288
+ return
289
+ # Trigger name found
290
+ self.context.try_new_function(token)
291
+ self.seen_trigger_name_token = False # Track if we've seen non-whitespace after name
292
+ self.next(self._trigger_after_name)
293
+
294
+ def _trigger_after_name(self, token):
295
+ """After trigger name, skip until DECLARE or BEGIN."""
296
+ token_lower = token.lower()
297
+
298
+ # Only check for dot immediately after trigger name (before any other tokens)
299
+ if token == "." and not self.seen_trigger_name_token:
300
+ # Schema-qualified name: the previous token was the schema,
301
+ # next non-whitespace token will be the actual trigger name
302
+ self.next(self._trigger_name_after_dot)
303
+ return
304
+
305
+ # Mark that we've seen a non-whitespace token after the trigger name
306
+ if not token.isspace() and token != "\n":
307
+ self.seen_trigger_name_token = True
308
+
309
+ if token_lower == "declare":
310
+ self.context.confirm_new_function()
311
+ self.next(self._state_before_begin)
312
+ elif token_lower == "begin":
313
+ self.context.confirm_new_function()
314
+ self.br_count = 1
315
+ self.next(self._state_body)
316
+ # Skip everything else (BEFORE/AFTER, INSERT/UPDATE/DELETE, ON table_name, FOR EACH ROW, etc.)
317
+
318
+ def _trigger_name_after_dot(self, token):
319
+ """Read the actual trigger name after schema.dot."""
320
+ if token.isspace() or token == "\n":
321
+ return
322
+ # Replace the previous (schema) name with the actual trigger name
323
+ self.context.current_function.name = token
324
+ self.seen_trigger_name_token = False # Reset for the real trigger name
325
+ self.next(self._trigger_after_name)
326
+
327
+ def _state_before_begin(self, token):
328
+ """
329
+ State between IS/AS and BEGIN - this is the declaration section.
330
+ Watch for nested procedures/functions and the BEGIN keyword.
331
+ """
332
+ token_lower = token.lower()
333
+
334
+ # Check for nested procedure/function declarations
335
+ if token_lower == "procedure":
336
+ self.declaring_nested_function = True
337
+ # Store current br_count level to know when nested function ends
338
+ if not hasattr(self, "nested_br_level"):
339
+ self.nested_br_level = 0
340
+ self.next(self._procedure_name)
341
+ return
342
+ elif token_lower == "function":
343
+ self.declaring_nested_function = True
344
+ # Store current br_count level to know when nested function ends
345
+ if not hasattr(self, "nested_br_level"):
346
+ self.nested_br_level = 0
347
+ self.next(self._function_name)
348
+ return
349
+ elif token_lower == "begin":
350
+ # Start of the implementation body
351
+ # Check if we had nested functions and need to reset br_count tracking
352
+ if hasattr(self, "nested_br_level"):
353
+ self.br_count = self.nested_br_level + 1
354
+ delattr(self, "nested_br_level")
355
+ else:
356
+ self.br_count = 1 # Initialize counter for the first BEGIN
357
+ self.next(self._state_body)
358
+
359
+ def _state_body(self, token):
360
+ """
361
+ Process function/procedure body.
362
+ Track control structures for cyclomatic complexity.
363
+ Manually track BEGIN/END blocks.
364
+ """
365
+ token_lower = token.lower()
366
+ token_upper = token.upper()
367
+
368
+ # Check for merged compound keywords like "END_IF", "END_LOOP", etc.
369
+ # These are created by the preprocessor
370
+ if token_lower.startswith("end_"):
371
+ # This is a compound END keyword, reset tracking
372
+ self.last_control_keyword = None
373
+ return
374
+
375
+ # Handle nested procedure/function declarations
376
+ if token_lower == "procedure":
377
+ self.next(self._procedure_name)
378
+ return
379
+ elif token_lower == "function":
380
+ self.next(self._function_name)
381
+ return
382
+
383
+ # Track FOR and WHILE to know when LOOP follows them
384
+ if token_upper in ("FOR", "WHILE"):
385
+ self.last_control_keyword = token_upper
386
+
387
+ # Handle LOOP keyword manually
388
+ # - Standalone LOOP adds +1 complexity
389
+ # - LOOP after FOR/WHILE does not add complexity (already counted for FOR/WHILE)
390
+ elif token_upper == "LOOP":
391
+ if self.last_control_keyword not in ("FOR", "WHILE"):
392
+ # This is a standalone LOOP, add complexity
393
+ self.context.add_condition()
394
+ # Reset tracking after processing LOOP
395
+ self.last_control_keyword = None
396
+
397
+ # PL/SQL uses BEGIN/END instead of {}
398
+ if token_lower == "begin":
399
+ self.br_count += 1
400
+ self.context.add_bare_nesting()
401
+ elif token_lower == "end":
402
+ # This is a standalone END (for BEGIN/END block)
403
+ self.br_count -= 1
404
+ if self.br_count == 0:
405
+ # This END closes the function/procedure
406
+ # Check if we have a parent function BEFORE ending (stack gets popped)
407
+ has_parent = len(self.context.stacked_functions) > 0
408
+ self.context.end_of_function()
409
+ # Return to appropriate state based on whether this was nested
410
+ if has_parent:
411
+ # Return to parent function's declaration section
412
+ self.next(self._state_before_begin)
413
+ else:
414
+ # No parent function, return to global
415
+ self.next(self._state_global)
416
+ return
417
+ else:
418
+ self.context.pop_nesting()
419
+
420
+ # Note: Basic conditions (if, elsif, when, while, for, and, or)
421
+ # are automatically counted by the condition_counter processor
422
+ # based on the _conditions set in the Reader class.
@@ -29,10 +29,12 @@ class PythonReader(CodeReader, ScriptLanguageMixIn):
29
29
 
30
30
  ext = ['py']
31
31
  language_names = ['python']
32
- _conditions = set([
33
- 'if', 'for', 'while', 'and', 'or',
34
- 'elif', 'except', 'finally'
35
- ])
32
+
33
+ # Separated condition categories
34
+ _control_flow_keywords = {'if', 'elif', 'for', 'while', 'except', 'finally'}
35
+ _logical_operators = {'and', 'or'}
36
+ _case_keywords = set() # Python uses if/elif, not case
37
+ _ternary_operators = set() # Python uses 'x if c else y' syntax, not ?
36
38
 
37
39
  def __init__(self, context):
38
40
  super(PythonReader, self).__init__(context)
lizard_languages/r.py CHANGED
@@ -12,12 +12,15 @@ class RReader(CodeReader, ScriptLanguageMixIn):
12
12
  ext = ['r', 'R']
13
13
  language_names = ['r', 'R']
14
14
 
15
- # R-specific conditions that increase cyclomatic complexity
16
- _conditions = {
17
- 'if', 'else if', 'for', 'while', 'repeat', 'switch',
18
- '&&', '||', '&', '|', 'ifelse',
19
- 'tryCatch', 'try'
20
- }
15
+ # Separated condition categories
16
+ _control_flow_keywords = {'if', 'else if', 'for', 'while', 'repeat', 'switch',
17
+ 'tryCatch', 'try', 'ifelse'} # ifelse is a vectorized control function
18
+ # R has both short-circuit (&&, ||) and element-wise (&, |) operators
19
+ # Both types count toward CCN as they represent conditional logic (vectorized or not)
20
+ # Users can use -Enonstrict to exclude logical operators if desired
21
+ _logical_operators = {'&&', '||', '&', '|'}
22
+ _case_keywords = set()
23
+ _ternary_operators = set()
21
24
 
22
25
  def __init__(self, context):
23
26
  super(RReader, self).__init__(context)
@@ -97,9 +97,12 @@ class RubylikeStateMachine(CodeStateMachine):
97
97
  class RubylikeReader(CodeReader, ScriptLanguageMixIn):
98
98
  # pylint: disable=R0903
99
99
 
100
- _conditions = set(['if', 'until', 'for', 'while', 'and', 'or',
101
- 'elsif', 'elseif', 'rescue',
102
- 'ensure', 'when', '||', '&&', '?'])
100
+ # Separated condition categories
101
+ _control_flow_keywords = {'if', 'elsif', 'elseif', 'until', 'for', 'while',
102
+ 'rescue', 'ensure', 'when'}
103
+ _logical_operators = {'and', 'or', '||', '&&'} # Both word and symbol forms
104
+ _case_keywords = set() # Ruby uses 'when' for case expressions
105
+ _ternary_operators = {'?'}
103
106
 
104
107
  def __init__(self, context):
105
108
  super(RubylikeReader, self).__init__(context)
lizard_languages/rust.py CHANGED
@@ -12,8 +12,13 @@ class RustReader(CodeReader, CCppCommentsMixin):
12
12
 
13
13
  ext = ['rs']
14
14
  language_names = ['rust']
15
- _conditions = set(['if', 'for', 'while', '&&', '||', '?', 'catch',
16
- 'case', 'match', 'where'])
15
+
16
+ # Separated condition categories
17
+ _control_flow_keywords = {'if', 'for', 'while', 'catch', 'match', 'where'}
18
+ _logical_operators = {'&&', '||'}
19
+ _case_keywords = set() # Rust uses match arms, not case keyword
20
+ # Note: '?' in Rust is the error propagation operator, not ternary
21
+ _ternary_operators = {'?'}
17
22
 
18
23
  def __init__(self, context):
19
24
  super().__init__(context)
lizard_languages/scala.py CHANGED
@@ -12,8 +12,12 @@ class ScalaReader(CodeReader, CCppCommentsMixin):
12
12
 
13
13
  ext = ['scala']
14
14
  language_names = ['scala']
15
- _conditions = set(['if', 'for', 'while', '&&', '||', '?', 'catch',
16
- 'case', 'do'])
15
+
16
+ # Separated condition categories
17
+ _control_flow_keywords = {'if', 'for', 'while', 'catch', 'do'}
18
+ _logical_operators = {'&&', '||'}
19
+ _case_keywords = {'case'} # Pattern matching
20
+ _ternary_operators = {'?'}
17
21
 
18
22
  def __init__(self, context):
19
23
  super(ScalaReader, self).__init__(context)