lizard 1.18.0__py2.py3-none-any.whl → 1.19.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lizard
3
- Version: 1.18.0
3
+ Version: 1.19.0
4
4
  Summary: A code analyzer without caring the C/C++ header files. It works with Java, C/C++, JavaScript, Python, Ruby, Swift, Objective C. Metrics includes cyclomatic complexity number etc.
5
5
  Home-page: http://www.lizard.ws
6
6
  Download-URL: https://pypi.python.org/lizard/
@@ -50,31 +50,32 @@ code analysis.
50
50
 
51
51
  A list of supported languages:
52
52
 
53
+ - C# (C Sharp)
53
54
  - C/C++ (works with C++14)
55
+ - Erlang
56
+ - Fortran
57
+ - GDScript
58
+ - Golang
54
59
  - Java
55
- - C# (C Sharp)
56
60
  - JavaScript (With ES6 and JSX)
57
- - TypeScript (With TSX)
58
- - VueJS
61
+ - Kotlin
62
+ - Lua
59
63
  - Objective-C
60
- - Swift
64
+ - Perl
65
+ - PHP
66
+ - PL/SQL
61
67
  - Python
68
+ - R
62
69
  - Ruby
63
- - TTCN-3
64
- - PHP
65
- - Scala
66
- - GDScript
67
- - Golang
68
- - Lua
69
70
  - Rust
70
- - Fortran
71
- - Kotlin
71
+ - Scala
72
72
  - Solidity
73
- - Erlang
74
- - Zig
75
- - Perl
76
73
  - Structured Text (St)
77
- - R
74
+ - Swift
75
+ - TTCN-3
76
+ - TypeScript (With TSX)
77
+ - VueJS
78
+ - Zig
78
79
 
79
80
  By default lizard will search for any source code that it knows and mix
80
81
  all the results together. This might not be what you want. You can use
@@ -162,7 +163,7 @@ Options
162
163
  search for all languages it knows. `lizard -l cpp -l java`searches for
163
164
  C++ and Java code. The available languages are: cpp, java, csharp,
164
165
  javascript, python, objectivec, ttcn, ruby, php, swift, scala, GDScript,
165
- go, lua, rust, typescript
166
+ go, lua, rust, typescript, plsql
166
167
  -V, --verbose Output in verbose mode (long function name)
167
168
  -C CCN, --CCN CCN Threshold for cyclomatic complexity number warning. The default value is
168
169
  15. Functions with CCN bigger than it will generate warning
@@ -197,7 +198,7 @@ Options
197
198
  -X, --xml Generate XML in cppncss style instead of the tabular output. Useful to
198
199
  generate report in Jenkins server
199
200
  --csv Generate CSV output as a transform of the default output
200
- -H, --html Output HTML report
201
+ -H, --html Output HTML report with interactive DataTables (sortable, searchable, filterable)
201
202
  --checkstyle Generate Checkstyle XML output for integration with Jenkins and other tools
202
203
  -m, --modified Calculate modified cyclomatic complexity number , which count a
203
204
  switch/case with multiple cases as one CCN.
@@ -415,5 +416,14 @@ Lizard is also used as a plugin for fastlane to help check code complexity and s
415
416
  - `European research project FASTEN (Fine-grained Analysis of SofTware Ecosystems as Networks, <http://fasten-project.eu/)>`_
416
417
  - `for a quality analyzer <https://github.com/fasten-project/quality-analyzer>`_
417
418
 
419
+ How To Contribute
420
+ -----------------
421
+
422
+ Contributions are welcome. Please refer to the rules and development workflow in:
423
+
424
+ - https://github.com/terryyin/lizard/tree/master/.cursor/rules
425
+
426
+ These guidelines are usable by both AI assistants and human contributors — what works for AI works for "I" as well — to keep changes cohesive, simple, and well-tested.
427
+
418
428
 
419
429
 
@@ -5,7 +5,7 @@ lizard_ext/checkstyleoutput.py,sha256=UzDHg837ErEZepXkR8I8YCoz2r1lkmzGctMA7dpyB-
5
5
  lizard_ext/csvoutput.py,sha256=43fhmo8kB85qcdujCwySGNuTC4FkKUPLqIApPeljPnA,2663
6
6
  lizard_ext/default_ordered_dict.py,sha256=YbVz6nPlQ6DjWc_EOFBz6AJN2XLo9dpnUdeyejQvUDE,831
7
7
  lizard_ext/extension_base.py,sha256=rnjUL2mqSGToUVYydju7fa8ZwynLPY8S1F17gIJP55I,346
8
- lizard_ext/htmloutput.py,sha256=oavhEzCIJtoJrFveiWIp0pMAOwg4pukJ6l07IpmPiag,4426
8
+ lizard_ext/htmloutput.py,sha256=G7MdLD7AwuR0LLCAkEpyHx4N_ssCcFQdXd7Xg5jZWX0,6537
9
9
  lizard_ext/keywords.py,sha256=VxsxoATtKV-8egMKd7I8sd2qbZMtEFEpsszk__6rmjQ,893
10
10
  lizard_ext/lizardboolcount.py,sha256=abmMA9X3VFRO5mziicUxWKmHldHNC0jBEe7NKAKA5fs,1062
11
11
  lizard_ext/lizardcomplextags.py,sha256=flrwYg24P5DoDsBO3gdcK9SxkugX_brhfjuu8zgPnOc,681
@@ -20,17 +20,17 @@ lizard_ext/lizardignoreassert.py,sha256=sqLwcnJQ06SYqIk901ib4NQ8ECwjIe_qL4T6z1wL
20
20
  lizard_ext/lizardio.py,sha256=xQN-AgLGLKJarJkgfaqX_TKyupbb7GTcwPxrL2B1J1w,3357
21
21
  lizard_ext/lizardmccabe.py,sha256=RiO8ASmQUah4udOH8SbE2OOMxwShIPByW93TlFxXlQU,1274
22
22
  lizard_ext/lizardmodified.py,sha256=4Ld7yy1D2m2biMtx-g0DtjXwLa-9mG2togS2IRDAF3k,705
23
- lizard_ext/lizardnd.py,sha256=2_uZkRlaM1mDLLW8yXtO7MtjMGklU8_Cy-6ocaILEuo,4391
23
+ lizard_ext/lizardnd.py,sha256=h4MaSsFAY5rK8kOSr61LkWntwJWV1KnhaEOWZgp3-c0,7496
24
24
  lizard_ext/lizardnonstrict.py,sha256=pPG22up2uh9rEkdRFtTWdiuOaiBNe0ZUjaZQpSTX5LE,394
25
25
  lizard_ext/lizardns.py,sha256=8pztUoRS_UWN24MawwxeHEJgYh49id5PWODUBb6O72U,4184
26
26
  lizard_ext/lizardoutside.py,sha256=FGm2tbBZ17-2OCgmQlD-vobUCfQKb0FAygf86eM3xuM,336
27
27
  lizard_ext/lizardstatementcount.py,sha256=xYk6ixSIItSE1BWQXzrWmduFgGhA3VR817SNKLffyVQ,1182
28
28
  lizard_ext/lizardwordcount.py,sha256=2QYXD7-AtkkgAbi9VSidunMbSsGQ7MKYb6IT-bS-cok,7575
29
- lizard_ext/version.py,sha256=WiI5ub_2twuTf6tscOzwP6MNdZV6kq-ElL2-2HqdUXc,181
29
+ lizard_ext/version.py,sha256=gScBFBQFiyhdj77Gb-x1hY6wABdKwZlb_aaNdiiADQ0,181
30
30
  lizard_ext/xmloutput.py,sha256=-cbh0he4O_X-wX56gkv9AnSPNN0qvR7FACqlBeezUS4,5609
31
- lizard_languages/__init__.py,sha256=NsYspQ6h--XHMJrIcmOxlZRF4f_l5M7JprenLBz2oIE,1559
32
- lizard_languages/clike.py,sha256=RICnhzBzLbMwpceo3X-z7_-hcTZmGAZKGqpKgJf0G0o,13598
33
- lizard_languages/code_reader.py,sha256=IfEHg9lzKnyCipX9xscgyGEOovll5qr9dCe5cSX2sJM,6852
31
+ lizard_languages/__init__.py,sha256=DniggHM3SOFlwkP8Tm4csEcisbf7_i1EvfoAkG9Atg0,1611
32
+ lizard_languages/clike.py,sha256=_oDUbA1hEIPujKw3wyAtC1uFPvjmg4TBgKbHLa-S1wg,13597
33
+ lizard_languages/code_reader.py,sha256=O0nZkI4xEHIWGgFBDwczj8-ay7aKllPnzRsblQtL3Gc,6874
34
34
  lizard_languages/csharp.py,sha256=EfFAIOIcJXUUhXTlZApXGSlzG34NZvHM9OSe6m7hpv0,2141
35
35
  lizard_languages/erlang.py,sha256=7YJS2cMyXDKEV_kpH8DzBARxFCFcjKuTOPSQ3K52auU,3860
36
36
  lizard_languages/fortran.py,sha256=KATDsnfjob5W3579A_VxbwrbTkK7Rx3p0eXdBgjx25I,8973
@@ -45,6 +45,7 @@ lizard_languages/lua.py,sha256=3nqBcunBzJrhv4Iqaf8xvbyqxZy3aSxJ-IiHimHFlac,1573
45
45
  lizard_languages/objc.py,sha256=2a1teLdaXZBtCeFiIZer1j_sVx9LZ1CbF2XfnqlvLmk,2319
46
46
  lizard_languages/perl.py,sha256=136w620eECe_t-kmlRUGrsZSxQNo2JQ_PZTSQfCSmHY,11987
47
47
  lizard_languages/php.py,sha256=UV40p8WzNC64NQ5qElPKzcFTjVt5kenLMz-eKYlcnMY,9940
48
+ lizard_languages/plsql.py,sha256=17zf0AkDLFW7mA3ngUNrU7z6U6DkmA96CKOcEhYNC5Q,17137
48
49
  lizard_languages/python.py,sha256=AsL0SmQ73zhNS1iGi4Z8VtuUE0VjqBzo9W8W0mjqL0E,5790
49
50
  lizard_languages/r.py,sha256=IoyMhmFtUmTji6rm6-fqss_j_kvIHu3JjABRh6RNys0,12583
50
51
  lizard_languages/ruby.py,sha256=HL1ZckeuUUJU3QSVAOPsG_Zsl0C6X2PX5_VaWqclzkM,2277
@@ -58,12 +59,12 @@ lizard_languages/swift.py,sha256=p8S2OAkQOx9YQ02yhoVXFkr7pMqUH1Nb3RVXPHRU_9M,245
58
59
  lizard_languages/tnsdl.py,sha256=pGcalA_lHY362v2wwPS86seYBOOBBjvmU6vd4Yy3A9g,2803
59
60
  lizard_languages/tsx.py,sha256=1oOVCcz5yHkmYLYGhSarCMSXfGVasweklAqqapkuNR4,17160
60
61
  lizard_languages/ttcn.py,sha256=ygjw_raBmPF-4mgoM8m6CAdyEMpTI-n1kZJK1RL4Vxo,2131
61
- lizard_languages/typescript.py,sha256=C8VWmHLGGYwCrP6hJ9HmJ-PETrKux4npjOzDN6AkTGU,12347
62
+ lizard_languages/typescript.py,sha256=unCDj040dY9fTOw9iIykqjt2j5tZWJ2Bm9fHYjOWY5I,14706
62
63
  lizard_languages/vue.py,sha256=KXUBUo2R1zNF8Pffrz_KsQEN44m5XFRMoGXylxKUeT0,1038
63
64
  lizard_languages/zig.py,sha256=NX1iyBstBuJFeAGBOAIaRfrmeBREne2HX6Pt4fXZZTQ,586
64
- lizard-1.18.0.dist-info/LICENSE.txt,sha256=05ZjgQ8Cl1dD9p0BhW-Txzkc5rhCogGJVEuf1GT2Y_M,1303
65
- lizard-1.18.0.dist-info/METADATA,sha256=BX2GeLwA0iQpWmDi0wYg4xXCTISh6Un8alaLcJhuRxc,16260
66
- lizard-1.18.0.dist-info/WHEEL,sha256=Kh9pAotZVRFj97E15yTA4iADqXdQfIVTHcNaZTjxeGM,110
67
- lizard-1.18.0.dist-info/entry_points.txt,sha256=pPMMwoHAltzGHqR2WeJQItLeeyR7pbX5R2S_POC-xoo,40
68
- lizard-1.18.0.dist-info/top_level.txt,sha256=5NTrTaOLhHuTzXaGcZPKfuaOgUv7WafNGe0Zl5aycpg,35
69
- lizard-1.18.0.dist-info/RECORD,,
65
+ lizard-1.19.0.dist-info/LICENSE.txt,sha256=05ZjgQ8Cl1dD9p0BhW-Txzkc5rhCogGJVEuf1GT2Y_M,1303
66
+ lizard-1.19.0.dist-info/METADATA,sha256=DNAVbhrtdVrI5tn2rWZoSBwCfZ5ArhMQ0XNmHvOqbh4,16697
67
+ lizard-1.19.0.dist-info/WHEEL,sha256=Kh9pAotZVRFj97E15yTA4iADqXdQfIVTHcNaZTjxeGM,110
68
+ lizard-1.19.0.dist-info/entry_points.txt,sha256=pPMMwoHAltzGHqR2WeJQItLeeyR7pbX5R2S_POC-xoo,40
69
+ lizard-1.19.0.dist-info/top_level.txt,sha256=5NTrTaOLhHuTzXaGcZPKfuaOgUv7WafNGe0Zl5aycpg,35
70
+ lizard-1.19.0.dist-info/RECORD,,
lizard_ext/htmloutput.py CHANGED
@@ -48,6 +48,12 @@ TEMPLATE = '''<!DOCTYPE HTML PUBLIC
48
48
  <head>
49
49
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
50
50
  <title>Code complexity report</title>
51
+ <!-- DataTables CSS -->
52
+ <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css">
53
+ <!-- jQuery -->
54
+ <script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
55
+ <!-- DataTables JS -->
56
+ <script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
51
57
  <style>
52
58
  h2
53
59
  {
@@ -88,6 +94,42 @@ TEMPLATE = '''<!DOCTYPE HTML PUBLIC
88
94
  font-family: sans-serif;
89
95
  white-space: nowrap;
90
96
  }
97
+ td.file-header
98
+ {
99
+ background-color: LightBlue;
100
+ font-weight: bold;
101
+ }
102
+ td.function-name
103
+ {
104
+ background-color: LightSteelBlue;
105
+ }
106
+ /* DataTables wrapper styling */
107
+ .dataTables_wrapper {
108
+ margin: 0 auto;
109
+ width: 95%;
110
+ }
111
+ /* Fallback styling if DataTables CSS doesn't load */
112
+ table#complexityTable {
113
+ border-collapse: collapse;
114
+ width: 95%;
115
+ margin: 0 auto;
116
+ }
117
+ table#complexityTable th {
118
+ background-color: #4CAF50;
119
+ color: white;
120
+ padding: 10px;
121
+ text-align: left;
122
+ border: 1px solid #ddd;
123
+ }
124
+ table#complexityTable td {
125
+ border: 1px solid #ddd;
126
+ }
127
+ table#complexityTable tr:nth-child(even) {
128
+ background-color: #f2f2f2;
129
+ }
130
+ table#complexityTable tr:hover {
131
+ background-color: #ddd;
132
+ }
91
133
  </style>
92
134
  </head>
93
135
  <body>
@@ -95,26 +137,29 @@ TEMPLATE = '''<!DOCTYPE HTML PUBLIC
95
137
 
96
138
  <center>
97
139
 
98
- <table>
140
+ <table id="complexityTable" class="display" style="width:100%">
141
+ <thead>
142
+ <tr>
143
+ <th>File</th>
144
+ <th>Function name</th>
145
+ <th>Cyclomatic complexity ({{ thresholds["cyclomatic_complexity"] }})</th>
146
+ <th>LOC ({{ thresholds["nloc"] }})</th>
147
+ <th>
148
+ {% if thresholds["token_count"] %}
149
+ Token count ({{ thresholds["token_count"] }})
150
+ {% else %}
151
+ Token count
152
+ {% endif %}
153
+ </th>
154
+ <th>Parameter count ({{ thresholds["parameter_count"] }})</th>
155
+ </tr>
156
+ </thead>
157
+ <tbody>
99
158
  {% for file in files %}
100
- <tr><td colspan="7" style="background-color:LightBlue;">
101
- Source file: <b>{{ file.filename }}</b></td></tr>
102
- {% if file.functions|length > 0 %}
103
- <tr><th>Function name</th><th></th><th>
104
- Cyclomatic complexity
105
- ({{ thresholds["cyclomatic_complexity"] }})
106
- </th><th>LOC ({{ thresholds["nloc"] }})</th><th>
107
- {% if thresholds["token_count"] %}
108
- Token count ({{ thresholds["token_count"] }})
109
- {% else %}
110
- Token count
111
- {% endif %}
112
- </th><th>Parameter count ({{ thresholds["parameter_count"] }})</th></tr>
113
- {% endif %}
114
159
  {% for func in file.functions %}
115
160
  <tr>
116
- <td style="background-color:LightSteelBlue">{{ func.name }}</td>
117
- <td></td>
161
+ <td>{{ file.filename }}</td>
162
+ <td class="function-name">{{ func.name }}</td>
118
163
  {% if func.cyclomatic_complexity > thresholds["cyclomatic_complexity"] %}
119
164
  <td class="greater-value">{{ func.cyclomatic_complexity }}</td>
120
165
  {% else %}
@@ -145,6 +190,7 @@ Cyclomatic complexity
145
190
  </tr>
146
191
  {% endfor %}
147
192
  {% endfor %}
193
+ </tbody>
148
194
  </table>
149
195
  <center>
150
196
 
@@ -154,6 +200,29 @@ Cyclomatic complexity
154
200
  <a href="http://www.lizard.ws/">Lizard</a> on {{ date }}
155
201
  </td></tr>
156
202
  </table>
203
+
204
+ <script>
205
+ // Gracefully degrade if CDN is unavailable
206
+ if (typeof jQuery !== 'undefined' && typeof jQuery.fn.dataTable !== 'undefined') {
207
+ $(document).ready(function() {
208
+ try {
209
+ $('#complexityTable').DataTable({
210
+ "pageLength": 25,
211
+ "order": [[2, "desc"]], // Sort by cyclomatic complexity descending by default
212
+ "columnDefs": [
213
+ { "type": "num", "targets": [2, 3, 4, 5] } // Ensure numeric sorting for metric columns
214
+ ]
215
+ });
216
+ } catch (e) {
217
+ console.warn('DataTables initialization failed. Displaying static table.', e);
218
+ }
219
+ });
220
+ } else {
221
+ // Fallback: Table will display as static HTML
222
+ console.info('DataTables not available. Displaying static HTML table.');
223
+ }
224
+ </script>
225
+
157
226
  </body>
158
227
  </html>
159
228
 
lizard_ext/lizardnd.py CHANGED
@@ -45,11 +45,43 @@ class LizardExtension(object): # pylint: disable=R0903
45
45
  else:
46
46
  indent_indicator = ';'
47
47
  for token in tokens:
48
- if token in loops:
48
+ # Handle opening parenthesis - start of condition
49
+ if token == '(':
50
+ reader.context.set_in_condition(True)
51
+ reader.context.increment_condition_depth()
52
+ reader.context.set_logical_operator_added(False) # Reset for new condition
53
+ # Handle closing parenthesis - end of condition
54
+ elif token == ')':
55
+ reader.context.decrement_condition_depth()
56
+ if reader.context.get_condition_depth() == 0:
57
+ reader.context.set_in_condition(False)
58
+ reader.context.set_logical_operator_added(False)
59
+
60
+ # Handle logical operators && and || within conditions
61
+ elif token in ('&&', '||'):
62
+ if reader.context.get_in_condition():
63
+ # Only add nesting depth for the first logical operator in a condition
64
+ # Subsequent && or || operators in the same condition don't add depth
65
+ if not reader.context.get_logical_operator_added():
66
+ l_depth = reader.context.add_nd_condition()
67
+ if not reader.context.get_loop_status():
68
+ reader.context.add_hidden_bracket_condition()
69
+ reader.context.loop_bracket_status()
70
+ reader.context.set_logical_operator_added(True)
71
+ else:
72
+ # Not in a condition, treat as regular nesting
73
+ l_depth = reader.context.add_nd_condition()
74
+ if not reader.context.get_loop_status():
75
+ reader.context.add_hidden_bracket_condition()
76
+ reader.context.loop_bracket_status()
77
+
78
+ # Handle other loop keywords (if, for, while, etc.)
79
+ elif token in loops and token not in ('&&', '||'):
49
80
  l_depth = reader.context.add_nd_condition()
50
81
  if not reader.context.get_loop_status():
51
82
  reader.context.add_hidden_bracket_condition()
52
83
  reader.context.loop_bracket_status()
84
+
53
85
  if token == loop_indicator:
54
86
  reader.context.loop_bracket_status()
55
87
  if token == bracket:
@@ -82,6 +114,7 @@ class NDFileInfoAddition(FileInfoBuilder):
82
114
  self.current_function.nesting_depth = 0
83
115
  self.current_function.hidden_bracket = 0
84
116
  self.current_function.bracket_loop = False
117
+ self.reset_condition_tracking()
85
118
 
86
119
  def add_hidden_bracket_condition(self, inc=1):
87
120
  self.current_function.hidden_bracket += inc
@@ -96,6 +129,33 @@ class NDFileInfoAddition(FileInfoBuilder):
96
129
  def get_loop_status(self):
97
130
  return self.current_function.bracket_loop
98
131
 
132
+ def set_in_condition(self, in_condition):
133
+ self.current_function.in_condition = in_condition
134
+
135
+ def get_in_condition(self):
136
+ return self.current_function.in_condition
137
+
138
+ def increment_condition_depth(self):
139
+ self.current_function.condition_depth += 1
140
+
141
+ def decrement_condition_depth(self):
142
+ if self.current_function.condition_depth > 0:
143
+ self.current_function.condition_depth -= 1
144
+
145
+ def get_condition_depth(self):
146
+ return self.current_function.condition_depth
147
+
148
+ def reset_condition_tracking(self):
149
+ self.current_function.in_condition = False
150
+ self.current_function.condition_depth = 0
151
+ self.current_function.logical_operator_added = False
152
+
153
+ def set_logical_operator_added(self, added):
154
+ self.current_function.logical_operator_added = added
155
+
156
+ def get_logical_operator_added(self):
157
+ return self.current_function.logical_operator_added
158
+
99
159
 
100
160
  def get_method(cls, name):
101
161
  """ python3 doesn't need the __func__ to get the func of the
@@ -127,6 +187,9 @@ def _init_nesting_depth_data(self, *_):
127
187
  self.max_nesting_depth = 0
128
188
  self.hidden_bracket = 0
129
189
  self.bracket_loop = False
190
+ self.in_condition = False # Track if we're inside a condition
191
+ self.condition_depth = 0 # Track nesting depth within conditions
192
+ self.logical_operator_added = False # Track if we've added nesting for logical operators in current condition
130
193
 
131
194
 
132
195
  patch(NDFileInfoAddition, FileInfoBuilder)
lizard_ext/version.py CHANGED
@@ -3,4 +3,4 @@
3
3
  #
4
4
  # pylint: disable=missing-docstring,invalid-name
5
5
 
6
- version = "1.18.0"
6
+ version = "1.19.0"
@@ -26,6 +26,7 @@ from .vue import VueReader
26
26
  from .perl import PerlReader
27
27
  from .st import StReader
28
28
  from .r import RReader
29
+ from .plsql import PLSQLReader
29
30
 
30
31
 
31
32
  def languages():
@@ -56,6 +57,7 @@ def languages():
56
57
  PerlReader,
57
58
  StReader,
58
59
  RReader,
60
+ PLSQLReader,
59
61
  ]
60
62
 
61
63
 
lizard_languages/clike.py CHANGED
@@ -54,7 +54,7 @@ class CLikeReader(CodeReader, CCppCommentsMixin):
54
54
  elif macro.group(1) == 'include':
55
55
  yield "#include"
56
56
  yield macro.group(2) or "\"\""
57
- for _ in macro.group(2).splitlines()[1:]:
57
+ for _ in macro.group(2).split('\n')[1:]:
58
58
  yield '\n'
59
59
  else:
60
60
  yield token
@@ -138,7 +138,7 @@ class CodeReader:
138
138
  r"|\/\/" + _until_end +
139
139
  r"|\#" +
140
140
  r"|:=|::|\*\*" +
141
- r"|\<\s*\?(?:\s*extends\s+\w+)?\s*\>" +
141
+ r"|\<(?=(?:[^<>]*\?)+[^<>]*\>)(?:[\w\s,.?]|(?:extends))+\>" +
142
142
  r"|" + r"|".join(re.escape(s) for s in combined_symbols) +
143
143
  r"|\\\n" +
144
144
  r"|\n" +
@@ -0,0 +1,419 @@
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
+ # PL/SQL conditions for cyclomatic complexity
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
+ _conditions = {"if", "elsif", "when", "while", "for", "and", "or"}
53
+
54
+ def __init__(self, context):
55
+ super(PLSQLReader, self).__init__(context)
56
+ self.parallel_states = [PLSQLStates(context)]
57
+ # PL/SQL is case-insensitive, so add both lowercase and uppercase versions
58
+ # of keywords to the conditions set for the automatic condition counter
59
+ self.conditions = self.conditions | {c.upper() for c in self.conditions}
60
+
61
+ def preprocess(self, tokens):
62
+ """
63
+ Preprocess tokens to handle PL/SQL-specific constructs.
64
+ Merge compound keywords to prevent the condition counter from double-counting:
65
+ - "END IF", "END LOOP", "END CASE", "END WHILE", "END FOR" -> single tokens
66
+ - "EXIT WHEN" -> remove the WHEN keyword (EXIT doesn't create a branch)
67
+ """
68
+ last_nonwhitespace_token = None
69
+ pending_tokens = []
70
+
71
+ for token in tokens:
72
+ if not token.isspace() or token == "\n":
73
+ token_upper = token.upper()
74
+
75
+ # Handle "END IF", "END LOOP", etc.
76
+ if (
77
+ last_nonwhitespace_token
78
+ and last_nonwhitespace_token.upper() == "END"
79
+ ):
80
+ if token_upper in ("IF", "LOOP", "CASE", "WHILE", "FOR"):
81
+ # Merge into "END_IF", "END_LOOP", etc.
82
+ yield "END_" + token_upper
83
+ last_nonwhitespace_token = None
84
+ pending_tokens = []
85
+ continue
86
+
87
+ # Handle "EXIT WHEN" - skip the WHEN keyword
88
+ if (
89
+ last_nonwhitespace_token
90
+ and last_nonwhitespace_token.upper() == "EXIT"
91
+ and token_upper == "WHEN"
92
+ ):
93
+ # Skip this WHEN keyword
94
+ pending_tokens = []
95
+ continue
96
+
97
+ # Yield any pending tokens
98
+ if last_nonwhitespace_token:
99
+ yield last_nonwhitespace_token
100
+ for pending in pending_tokens:
101
+ yield pending
102
+ pending_tokens = []
103
+
104
+ # Update tracking
105
+ last_nonwhitespace_token = token
106
+ else:
107
+ # Accumulate whitespace
108
+ pending_tokens.append(token)
109
+
110
+ # Don't forget the last tokens
111
+ if last_nonwhitespace_token:
112
+ yield last_nonwhitespace_token
113
+ for pending in pending_tokens:
114
+ yield pending
115
+
116
+ @staticmethod
117
+ def generate_tokens(source_code, addition="", token_class=None):
118
+ """
119
+ Generate tokens for PL/SQL code.
120
+ PL/SQL uses:
121
+ - Single-line comments: --
122
+ - Multi-line comments: /* */
123
+ - String literals: 'text' (with '' for escaping)
124
+ - Assignment operator: :=
125
+ """
126
+ # Add PL/SQL-specific patterns
127
+ addition = r"|--[^\n]*" + addition # Single-line comment starting with --
128
+ return CodeReader.generate_tokens(source_code, addition, token_class)
129
+
130
+ def get_comment_from_token(self, token):
131
+ """
132
+ Override to recognize PL/SQL's -- line comments in addition to /* */ block comments.
133
+ PL/SQL uses -- for single-line comments (like SQL standard).
134
+
135
+ Note: This method correctly identifies -- comments, but due to a limitation in
136
+ the NLOC calculation, these comments may still be counted in NLOC.
137
+ """
138
+ if token.startswith("--"):
139
+ return token # Return full comment token (like Lua does)
140
+ # Delegate to parent for /* */ and // comments
141
+ return super().get_comment_from_token(token)
142
+
143
+
144
+ class PLSQLStates(CodeStateMachine):
145
+ """
146
+ State machine for parsing PL/SQL code structure.
147
+ """
148
+
149
+ def __init__(self, context):
150
+ super(PLSQLStates, self).__init__(context)
151
+ self.in_parameter_list = False
152
+ self.last_control_keyword = None # Track FOR/WHILE to avoid counting their LOOP
153
+ self.declaring_nested_function = (
154
+ False # Track if we're declaring a nested function
155
+ )
156
+
157
+ def _state_global(self, token):
158
+ """Global state - looking for function/procedure/trigger declarations."""
159
+ token_lower = token.lower()
160
+
161
+ if token_lower == "procedure":
162
+ self.next(self._procedure_name)
163
+ elif token_lower == "function":
164
+ self.next(self._function_name)
165
+ elif token_lower == "trigger":
166
+ self.next(self._trigger_name)
167
+
168
+ def _procedure_name(self, token):
169
+ """Read procedure name."""
170
+ if token.isspace() or token == "\n":
171
+ return
172
+ if token == "(":
173
+ self.in_parameter_list = True
174
+ self.next(self._parameters, "(")
175
+ elif token.lower() in ("is", "as"):
176
+ self.context.confirm_new_function()
177
+ self.next(self._state_before_begin)
178
+ else:
179
+ # Check if this is a nested function
180
+ if self.declaring_nested_function:
181
+ self.context.push_new_function(token)
182
+ self.declaring_nested_function = False
183
+ else:
184
+ self.context.try_new_function(token)
185
+ self.next(self._procedure_after_name)
186
+
187
+ def _procedure_after_name(self, token):
188
+ """After procedure name, look for parameters or IS/AS."""
189
+ if token == ".":
190
+ # Schema-qualified name: the previous token was the schema,
191
+ # next non-whitespace token will be the actual procedure name
192
+ self.next(self._procedure_name_after_dot)
193
+ elif token == "(":
194
+ self.in_parameter_list = True
195
+ self.next(self._parameters, "(")
196
+ elif token.lower() in ("is", "as"):
197
+ self.context.confirm_new_function()
198
+ self.next(self._state_before_begin)
199
+ # Skip whitespace and other tokens
200
+
201
+ def _procedure_name_after_dot(self, token):
202
+ """Read the actual procedure name after schema.dot."""
203
+ if token.isspace() or token == "\n":
204
+ return
205
+ # Replace the previous (schema) name with the actual procedure name
206
+ self.context.current_function.name = token
207
+ self.next(self._procedure_after_name)
208
+
209
+ def _function_name(self, token):
210
+ """Read function name."""
211
+ if token.isspace() or token == "\n":
212
+ return
213
+ if token == "(":
214
+ self.in_parameter_list = True
215
+ self.next(self._parameters, "(")
216
+ elif token.lower() == "return":
217
+ self.next(self._return_type)
218
+ elif token.lower() in ("is", "as"):
219
+ self.context.confirm_new_function()
220
+ self.next(self._state_before_begin)
221
+ else:
222
+ # Check if this is a nested function
223
+ if self.declaring_nested_function:
224
+ self.context.push_new_function(token)
225
+ self.declaring_nested_function = False
226
+ else:
227
+ self.context.try_new_function(token)
228
+ self.next(self._function_after_name)
229
+
230
+ def _function_after_name(self, token):
231
+ """After function name, look for parameters, RETURN, or IS/AS."""
232
+ if token == ".":
233
+ # Schema-qualified name: the previous token was the schema,
234
+ # next non-whitespace token will be the actual function name
235
+ self.next(self._function_name_after_dot)
236
+ elif token == "(":
237
+ self.in_parameter_list = True
238
+ self.next(self._parameters, "(")
239
+ elif token.lower() == "return":
240
+ self.next(self._return_type)
241
+ elif token.lower() in ("is", "as"):
242
+ self.context.confirm_new_function()
243
+ self.next(self._state_before_begin)
244
+ # Skip whitespace and other tokens
245
+
246
+ def _function_name_after_dot(self, token):
247
+ """Read the actual function name after schema.dot."""
248
+ if token.isspace() or token == "\n":
249
+ return
250
+ # Replace the previous (schema) name with the actual function name
251
+ self.context.current_function.name = token
252
+ self.next(self._function_after_name)
253
+
254
+ def _return_type(self, token):
255
+ """Skip return type declaration."""
256
+ if token.lower() in ("is", "as"):
257
+ self.context.confirm_new_function()
258
+ self.next(self._state_before_begin)
259
+ # Skip everything else (return type tokens)
260
+
261
+ def _parameters(self, token):
262
+ """Read parameters."""
263
+ if token == ")":
264
+ self.in_parameter_list = False
265
+ self.next(self._after_parameters)
266
+ elif token == ",":
267
+ # Each comma separates parameters
268
+ self.context.parameter(token)
269
+ elif not token.isspace() and token != "\n":
270
+ # Track non-whitespace tokens as potential parameters
271
+ self.context.parameter(token)
272
+
273
+ def _after_parameters(self, token):
274
+ """After parameters, look for IS/AS or RETURN."""
275
+ if token.lower() == "return":
276
+ self.next(self._return_type)
277
+ elif token.lower() in ("is", "as"):
278
+ self.context.confirm_new_function()
279
+ self.next(self._state_before_begin)
280
+ # Skip whitespace and other tokens
281
+
282
+ def _trigger_name(self, token):
283
+ """Read trigger name."""
284
+ if token.isspace() or token == "\n":
285
+ return
286
+ # Trigger name found
287
+ self.context.try_new_function(token)
288
+ self.seen_trigger_name_token = False # Track if we've seen non-whitespace after name
289
+ self.next(self._trigger_after_name)
290
+
291
+ def _trigger_after_name(self, token):
292
+ """After trigger name, skip until DECLARE or BEGIN."""
293
+ token_lower = token.lower()
294
+
295
+ # Only check for dot immediately after trigger name (before any other tokens)
296
+ if token == "." and not self.seen_trigger_name_token:
297
+ # Schema-qualified name: the previous token was the schema,
298
+ # next non-whitespace token will be the actual trigger name
299
+ self.next(self._trigger_name_after_dot)
300
+ return
301
+
302
+ # Mark that we've seen a non-whitespace token after the trigger name
303
+ if not token.isspace() and token != "\n":
304
+ self.seen_trigger_name_token = True
305
+
306
+ if token_lower == "declare":
307
+ self.context.confirm_new_function()
308
+ self.next(self._state_before_begin)
309
+ elif token_lower == "begin":
310
+ self.context.confirm_new_function()
311
+ self.br_count = 1
312
+ self.next(self._state_body)
313
+ # Skip everything else (BEFORE/AFTER, INSERT/UPDATE/DELETE, ON table_name, FOR EACH ROW, etc.)
314
+
315
+ def _trigger_name_after_dot(self, token):
316
+ """Read the actual trigger name after schema.dot."""
317
+ if token.isspace() or token == "\n":
318
+ return
319
+ # Replace the previous (schema) name with the actual trigger name
320
+ self.context.current_function.name = token
321
+ self.seen_trigger_name_token = False # Reset for the real trigger name
322
+ self.next(self._trigger_after_name)
323
+
324
+ def _state_before_begin(self, token):
325
+ """
326
+ State between IS/AS and BEGIN - this is the declaration section.
327
+ Watch for nested procedures/functions and the BEGIN keyword.
328
+ """
329
+ token_lower = token.lower()
330
+
331
+ # Check for nested procedure/function declarations
332
+ if token_lower == "procedure":
333
+ self.declaring_nested_function = True
334
+ # Store current br_count level to know when nested function ends
335
+ if not hasattr(self, "nested_br_level"):
336
+ self.nested_br_level = 0
337
+ self.next(self._procedure_name)
338
+ return
339
+ elif token_lower == "function":
340
+ self.declaring_nested_function = True
341
+ # Store current br_count level to know when nested function ends
342
+ if not hasattr(self, "nested_br_level"):
343
+ self.nested_br_level = 0
344
+ self.next(self._function_name)
345
+ return
346
+ elif token_lower == "begin":
347
+ # Start of the implementation body
348
+ # Check if we had nested functions and need to reset br_count tracking
349
+ if hasattr(self, "nested_br_level"):
350
+ self.br_count = self.nested_br_level + 1
351
+ delattr(self, "nested_br_level")
352
+ else:
353
+ self.br_count = 1 # Initialize counter for the first BEGIN
354
+ self.next(self._state_body)
355
+
356
+ def _state_body(self, token):
357
+ """
358
+ Process function/procedure body.
359
+ Track control structures for cyclomatic complexity.
360
+ Manually track BEGIN/END blocks.
361
+ """
362
+ token_lower = token.lower()
363
+ token_upper = token.upper()
364
+
365
+ # Check for merged compound keywords like "END_IF", "END_LOOP", etc.
366
+ # These are created by the preprocessor
367
+ if token_lower.startswith("end_"):
368
+ # This is a compound END keyword, reset tracking
369
+ self.last_control_keyword = None
370
+ return
371
+
372
+ # Handle nested procedure/function declarations
373
+ if token_lower == "procedure":
374
+ self.next(self._procedure_name)
375
+ return
376
+ elif token_lower == "function":
377
+ self.next(self._function_name)
378
+ return
379
+
380
+ # Track FOR and WHILE to know when LOOP follows them
381
+ if token_upper in ("FOR", "WHILE"):
382
+ self.last_control_keyword = token_upper
383
+
384
+ # Handle LOOP keyword manually
385
+ # - Standalone LOOP adds +1 complexity
386
+ # - LOOP after FOR/WHILE does not add complexity (already counted for FOR/WHILE)
387
+ elif token_upper == "LOOP":
388
+ if self.last_control_keyword not in ("FOR", "WHILE"):
389
+ # This is a standalone LOOP, add complexity
390
+ self.context.add_condition()
391
+ # Reset tracking after processing LOOP
392
+ self.last_control_keyword = None
393
+
394
+ # PL/SQL uses BEGIN/END instead of {}
395
+ if token_lower == "begin":
396
+ self.br_count += 1
397
+ self.context.add_bare_nesting()
398
+ elif token_lower == "end":
399
+ # This is a standalone END (for BEGIN/END block)
400
+ self.br_count -= 1
401
+ if self.br_count == 0:
402
+ # This END closes the function/procedure
403
+ # Check if we have a parent function BEFORE ending (stack gets popped)
404
+ has_parent = len(self.context.stacked_functions) > 0
405
+ self.context.end_of_function()
406
+ # Return to appropriate state based on whether this was nested
407
+ if has_parent:
408
+ # Return to parent function's declaration section
409
+ self.next(self._state_before_begin)
410
+ else:
411
+ # No parent function, return to global
412
+ self.next(self._state_global)
413
+ return
414
+ else:
415
+ self.context.pop_nesting()
416
+
417
+ # Note: Basic conditions (if, elsif, when, while, for, and, or)
418
+ # are automatically counted by the condition_counter processor
419
+ # based on the _conditions set in the Reader class.
@@ -128,6 +128,9 @@ class TypeScriptStates(CodeStateMachine):
128
128
  self._getter_setter_prefix = None
129
129
  self.arrow_function_pending = False
130
130
  self._ts_declare = False # Track if 'declare' was seen
131
+ self._static_seen = False # Track if 'static' was seen
132
+ self._async_seen = False # Track if 'async' was seen
133
+ self._prev_token = '' # Track previous token to detect method calls
131
134
 
132
135
  def statemachine_before_return(self):
133
136
  # Ensure the main function is closed at the end
@@ -152,6 +155,20 @@ class TypeScriptStates(CodeStateMachine):
152
155
  return
153
156
  self._ts_declare = False
154
157
 
158
+ # Track static and async modifiers
159
+ if token == 'static':
160
+ self._static_seen = True
161
+ self._prev_token = token
162
+ return
163
+ if token == 'async':
164
+ self._async_seen = True
165
+ self._prev_token = token
166
+ return
167
+ if token == 'new':
168
+ # Track 'new' keyword to avoid treating constructors as functions
169
+ self._prev_token = token
170
+ return
171
+
155
172
  if self.as_object:
156
173
  # Support for getter/setter: look for 'get' or 'set' before method name
157
174
  if token in ('get', 'set'):
@@ -169,15 +186,26 @@ class TypeScriptStates(CodeStateMachine):
169
186
  self.function_name = self.last_tokens
170
187
  return
171
188
  elif token == '(':
189
+ # Check if this is a method call (previous token was . or this/identifier)
190
+ if self._prev_token == '.' or self._prev_token == 'new':
191
+ # This is a method call, not a function definition
192
+ self._prev_token = token
193
+ return
172
194
  if not self.started_function:
173
195
  self.arrow_function_pending = True
174
196
  self._function(self.last_tokens)
175
197
  self.next(self._function, token)
176
198
  return
199
+ # If we've seen async/static and this is an identifier, it's likely a method name
200
+ elif (self._async_seen or self._static_seen) and token not in ('*', 'function'):
201
+ # This is a method name after async/static
202
+ self.last_tokens = token
203
+ return
177
204
 
178
205
  if token in '.':
179
206
  self._state = self._field
180
207
  self.last_tokens += token
208
+ self._prev_token = token
181
209
  return
182
210
  if token == 'function':
183
211
  self._state = self._function
@@ -191,8 +219,14 @@ class TypeScriptStates(CodeStateMachine):
191
219
  elif token == '=':
192
220
  self.function_name = self.last_tokens
193
221
  elif token == "(":
194
- self.sub_state(
195
- self.__class__(self.context))
222
+ # Check if this is a method call or constructor
223
+ if self._prev_token == '.' or self._prev_token == 'new':
224
+ # This is a method call or constructor, not a function definition
225
+ self.sub_state(
226
+ self.__class__(self.context))
227
+ else:
228
+ self.sub_state(
229
+ self.__class__(self.context))
196
230
  elif token in '{':
197
231
  if self.started_function:
198
232
  self.sub_state(
@@ -205,14 +239,21 @@ class TypeScriptStates(CodeStateMachine):
205
239
  elif self.context.newline or token == ';':
206
240
  self.function_name = ''
207
241
  self._pop_function_from_stack()
242
+ # Reset modifiers on newline/semicolon
243
+ self._static_seen = False
244
+ self._async_seen = False
208
245
 
209
246
  if token == '`':
210
247
  self.next(self._state_template_literal)
211
248
  if not self.as_object:
212
249
  if token == ':':
213
250
  self._consume_type_annotation()
251
+ self._prev_token = token
214
252
  return
215
253
  self.last_tokens = token
254
+ # Don't overwrite _prev_token if it's 'new' or '.' (preserve for next token)
255
+ if self._prev_token not in ('new', '.'):
256
+ self._prev_token = token
216
257
 
217
258
  def read_object(self):
218
259
  def callback():
@@ -220,7 +261,13 @@ class TypeScriptStates(CodeStateMachine):
220
261
 
221
262
  object_reader = self.__class__(self.context)
222
263
  object_reader.as_object = True
264
+ # Pass along the modifier flags
265
+ object_reader._static_seen = self._static_seen
266
+ object_reader._async_seen = self._async_seen
223
267
  self.sub_state(object_reader, callback)
268
+ # Reset modifiers after entering object
269
+ self._static_seen = False
270
+ self._async_seen = False
224
271
 
225
272
  def _expecting_condition_and_statement_block(self, token):
226
273
  def callback():
@@ -269,6 +316,9 @@ class TypeScriptStates(CodeStateMachine):
269
316
  return
270
317
  if token != '(':
271
318
  self.function_name = token
319
+ # Reset modifiers after setting function name
320
+ self._static_seen = False
321
+ self._async_seen = False
272
322
  else:
273
323
  if not self.started_function:
274
324
  self._push_function_to_stack()