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.
- {lizard-1.18.0.dist-info → lizard-1.20.0.dist-info}/METADATA +30 -20
- lizard-1.20.0.dist-info/RECORD +70 -0
- lizard_ext/__init__.py +0 -1
- lizard_ext/htmloutput.py +86 -17
- lizard_ext/lizardcomplextags.py +6 -0
- lizard_ext/lizardmccabe.py +6 -1
- lizard_ext/lizardmodified.py +7 -2
- lizard_ext/lizardnd.py +64 -1
- lizard_ext/lizardnonstrict.py +2 -1
- lizard_ext/lizardns.py +31 -8
- lizard_ext/version.py +1 -1
- lizard_languages/__init__.py +2 -0
- lizard_languages/clike.py +6 -3
- lizard_languages/code_reader.py +27 -3
- lizard_languages/csharp.py +21 -2
- lizard_languages/erlang.py +9 -1
- lizard_languages/fortran.py +5 -6
- lizard_languages/gdscript.py +6 -2
- lizard_languages/kotlin.py +6 -3
- lizard_languages/perl.py +8 -4
- lizard_languages/php.py +6 -2
- lizard_languages/plsql.py +422 -0
- lizard_languages/python.py +6 -4
- lizard_languages/r.py +9 -6
- lizard_languages/rubylike.py +6 -3
- lizard_languages/rust.py +7 -2
- lizard_languages/scala.py +6 -2
- lizard_languages/solidity.py +6 -1
- lizard_languages/st.py +9 -5
- lizard_languages/swift.py +6 -2
- lizard_languages/tnsdl.py +6 -1
- lizard_languages/tsx.py +2 -2
- lizard_languages/ttcn.py +5 -3
- lizard_languages/typescript.py +58 -4
- lizard_languages/zig.py +7 -1
- lizard-1.18.0.dist-info/RECORD +0 -69
- {lizard-1.18.0.dist-info → lizard-1.20.0.dist-info}/LICENSE.txt +0 -0
- {lizard-1.18.0.dist-info → lizard-1.20.0.dist-info}/WHEEL +0 -0
- {lizard-1.18.0.dist-info → lizard-1.20.0.dist-info}/entry_points.txt +0 -0
- {lizard-1.18.0.dist-info → lizard-1.20.0.dist-info}/top_level.txt +0 -0
lizard_languages/csharp.py
CHANGED
|
@@ -11,8 +11,11 @@ class CSharpReader(CLikeReader):
|
|
|
11
11
|
ext = ['cs']
|
|
12
12
|
language_names = ['csharp']
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
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
|
lizard_languages/erlang.py
CHANGED
|
@@ -13,7 +13,15 @@ class ErlangReader(CodeReader):
|
|
|
13
13
|
|
|
14
14
|
ext = ['erl', 'hrl', 'es', 'escript']
|
|
15
15
|
language_names = ['erlang']
|
|
16
|
-
|
|
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)
|
lizard_languages/fortran.py
CHANGED
|
@@ -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
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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'
|
lizard_languages/gdscript.py
CHANGED
|
@@ -9,8 +9,12 @@ class GDScriptReader(PythonReader):
|
|
|
9
9
|
|
|
10
10
|
ext = ['gd']
|
|
11
11
|
language_names = ['GDScript']
|
|
12
|
-
|
|
13
|
-
|
|
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)
|
lizard_languages/kotlin.py
CHANGED
|
@@ -13,9 +13,12 @@ class KotlinReader(CodeReader, CCppCommentsMixin, SwiftReplaceLabel):
|
|
|
13
13
|
|
|
14
14
|
ext = ['kt', 'kts']
|
|
15
15
|
language_names = ['kotlin']
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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.
|
lizard_languages/python.py
CHANGED
|
@@ -29,10 +29,12 @@ class PythonReader(CodeReader, ScriptLanguageMixIn):
|
|
|
29
29
|
|
|
30
30
|
ext = ['py']
|
|
31
31
|
language_names = ['python']
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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)
|
lizard_languages/rubylike.py
CHANGED
|
@@ -97,9 +97,12 @@ class RubylikeStateMachine(CodeStateMachine):
|
|
|
97
97
|
class RubylikeReader(CodeReader, ScriptLanguageMixIn):
|
|
98
98
|
# pylint: disable=R0903
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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)
|