lizard 1.17.31__py2.py3-none-any.whl → 1.18.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.17.31
3
+ Version: 1.18.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/
@@ -73,6 +73,8 @@ A list of supported languages:
73
73
  - Erlang
74
74
  - Zig
75
75
  - Perl
76
+ - Structured Text (St)
77
+ - R
76
78
 
77
79
  By default lizard will search for any source code that it knows and mix
78
80
  all the results together. This might not be what you want. You can use
@@ -413,3 +415,5 @@ Lizard is also used as a plugin for fastlane to help check code complexity and s
413
415
  - `European research project FASTEN (Fine-grained Analysis of SofTware Ecosystems as Networks, <http://fasten-project.eu/)>`_
414
416
  - `for a quality analyzer <https://github.com/fasten-project/quality-analyzer>`_
415
417
 
418
+
419
+
@@ -1,7 +1,7 @@
1
- lizard.py,sha256=H0EgGsU0ou4IlRY9AxIyY_O4_WO8trZda4GGM7a4kNQ,41403
1
+ lizard.py,sha256=B0g5lEp5me31bQpjfMKVayF0iUfH0PalVecOKeUX3-A,41365
2
2
  lizard_ext/__init__.py,sha256=AkZYVqCgd7XX1hMwLMyh_ABm9geIHmobEch-thYXZz8,932
3
3
  lizard_ext/auto_open.py,sha256=byD_RbeVhvSUhR2bJMRitvA3zcKEapFwv0-XaDJ6GFo,1096
4
- lizard_ext/checkstyleoutput.py,sha256=puuTq5IfM48NkDdds2FjF1BFq1QDGfsf5GahLTVfN6Y,1213
4
+ lizard_ext/checkstyleoutput.py,sha256=UzDHg837ErEZepXkR8I8YCoz2r1lkmzGctMA7dpyB-M,1245
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
@@ -26,10 +26,10 @@ 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=N4oSGH5svpjDluQQOHRznK3TrM-OyaJq6FN_Yuc5pvw,182
29
+ lizard_ext/version.py,sha256=WiI5ub_2twuTf6tscOzwP6MNdZV6kq-ElL2-2HqdUXc,181
30
30
  lizard_ext/xmloutput.py,sha256=-cbh0he4O_X-wX56gkv9AnSPNN0qvR7FACqlBeezUS4,5609
31
- lizard_languages/__init__.py,sha256=ArNmUrVSU6HFxhDka1-vWMZpVIM39P-gqv6BoOLNMV8,1522
32
- lizard_languages/clike.py,sha256=INd5tkvwEVZm7dx2yHG2OIFHZn7JzQGmnT9WQNFZ2XU,11110
31
+ lizard_languages/__init__.py,sha256=NsYspQ6h--XHMJrIcmOxlZRF4f_l5M7JprenLBz2oIE,1559
32
+ lizard_languages/clike.py,sha256=RICnhzBzLbMwpceo3X-z7_-hcTZmGAZKGqpKgJf0G0o,13598
33
33
  lizard_languages/code_reader.py,sha256=IfEHg9lzKnyCipX9xscgyGEOovll5qr9dCe5cSX2sJM,6852
34
34
  lizard_languages/csharp.py,sha256=EfFAIOIcJXUUhXTlZApXGSlzG34NZvHM9OSe6m7hpv0,2141
35
35
  lizard_languages/erlang.py,sha256=7YJS2cMyXDKEV_kpH8DzBARxFCFcjKuTOPSQ3K52auU,3860
@@ -39,31 +39,31 @@ lizard_languages/go.py,sha256=sntz0jOEuj4klPipoTFd16UDK1fAUQfwK7YX_cLMZAc,1346
39
39
  lizard_languages/golike.py,sha256=vRIfjTVvc0VmJf27lTOLht55ZF1AQ9wn0Fvu-9WabWk,2858
40
40
  lizard_languages/java.py,sha256=HQBTZjUKbUJwgmtLYIzJrWtPpFP3ZdBP_NJK7YOXZC0,6424
41
41
  lizard_languages/javascript.py,sha256=vniCNMW-ea9Jpv6c8qCcjLVDYjT8VztjXigp5XRWt0E,317
42
- lizard_languages/js_style_language_states.py,sha256=6mLrHfvDC4oHttKsSRGkE-ayG8uKSEm4E4rlhaUN5lA,6396
43
42
  lizard_languages/js_style_regex_expression.py,sha256=Xgyogch4xElYtCG4EnBKvalHTl3tjRPcIIcIQRRd61I,1970
44
- lizard_languages/jsx.py,sha256=TLH93qrZO2r_tiiv1XBIbw2_mbUMjwCZAuBz5vG5oBw,11696
45
43
  lizard_languages/kotlin.py,sha256=v_o2orEzA5gB9vM_0h-E4QXjrc5Yum-0K6W6_laOThc,2844
46
44
  lizard_languages/lua.py,sha256=3nqBcunBzJrhv4Iqaf8xvbyqxZy3aSxJ-IiHimHFlac,1573
47
45
  lizard_languages/objc.py,sha256=2a1teLdaXZBtCeFiIZer1j_sVx9LZ1CbF2XfnqlvLmk,2319
48
46
  lizard_languages/perl.py,sha256=136w620eECe_t-kmlRUGrsZSxQNo2JQ_PZTSQfCSmHY,11987
49
47
  lizard_languages/php.py,sha256=UV40p8WzNC64NQ5qElPKzcFTjVt5kenLMz-eKYlcnMY,9940
50
- lizard_languages/python.py,sha256=4AK0JAPBSM7eKrqDdSaHfewCGAYMzdhUhi5kZK4u23k,3847
48
+ lizard_languages/python.py,sha256=AsL0SmQ73zhNS1iGi4Z8VtuUE0VjqBzo9W8W0mjqL0E,5790
49
+ lizard_languages/r.py,sha256=IoyMhmFtUmTji6rm6-fqss_j_kvIHu3JjABRh6RNys0,12583
51
50
  lizard_languages/ruby.py,sha256=HL1ZckeuUUJU3QSVAOPsG_Zsl0C6X2PX5_VaWqclzkM,2277
52
51
  lizard_languages/rubylike.py,sha256=dAGZ2wqW8nqaESMU8HkeR9gwQ-q9fmZqE6AANvVZD1Q,3426
53
- lizard_languages/rust.py,sha256=vfBBktNwIZL8AiaAJ5EW8o4rL7_TwjDo3QhpfxxecfE,596
52
+ lizard_languages/rust.py,sha256=WarDHnFZv99Yu3_C5DpZfLS8dVWz6AcOzo2dzLW94rA,817
54
53
  lizard_languages/scala.py,sha256=6Jr_TG945VYqB3o5weD7jN7S4beHt4aVj3r-fmKeMAM,1316
55
54
  lizard_languages/script_language.py,sha256=SKe45AbO6Z-axbN8KW_g7jf9g7YTXZ6dWzJj4ubDsM8,1172
56
55
  lizard_languages/solidity.py,sha256=Z0GD7U5bI5eUikdy7m_iKWeFD5yXRYq4r3zycscOhJQ,553
56
+ lizard_languages/st.py,sha256=7fpOfNAoUjNY8RCHSYLufnOzZTUkKwjVvcyRyM1xP2Y,4160
57
57
  lizard_languages/swift.py,sha256=p8S2OAkQOx9YQ02yhoVXFkr7pMqUH1Nb3RVXPHRU_9M,2450
58
58
  lizard_languages/tnsdl.py,sha256=pGcalA_lHY362v2wwPS86seYBOOBBjvmU6vd4Yy3A9g,2803
59
- lizard_languages/tsx.py,sha256=AgpULWV9VEF4wJGtNZeeOl6NuTiQOstM8P3jObz-_xY,1249
59
+ lizard_languages/tsx.py,sha256=1oOVCcz5yHkmYLYGhSarCMSXfGVasweklAqqapkuNR4,17160
60
60
  lizard_languages/ttcn.py,sha256=ygjw_raBmPF-4mgoM8m6CAdyEMpTI-n1kZJK1RL4Vxo,2131
61
- lizard_languages/typescript.py,sha256=fiEiGs0VO8uofsLcOD7t8UzKp0Un9SSbfQTU87mhNVo,5626
61
+ lizard_languages/typescript.py,sha256=C8VWmHLGGYwCrP6hJ9HmJ-PETrKux4npjOzDN6AkTGU,12347
62
62
  lizard_languages/vue.py,sha256=KXUBUo2R1zNF8Pffrz_KsQEN44m5XFRMoGXylxKUeT0,1038
63
63
  lizard_languages/zig.py,sha256=NX1iyBstBuJFeAGBOAIaRfrmeBREne2HX6Pt4fXZZTQ,586
64
- lizard-1.17.31.dist-info/LICENSE.txt,sha256=05ZjgQ8Cl1dD9p0BhW-Txzkc5rhCogGJVEuf1GT2Y_M,1303
65
- lizard-1.17.31.dist-info/METADATA,sha256=8P82zQ9bK0enmWJ9bOSezN6Ly1oO5WvgfWO6i0jSEx4,16230
66
- lizard-1.17.31.dist-info/WHEEL,sha256=Kh9pAotZVRFj97E15yTA4iADqXdQfIVTHcNaZTjxeGM,110
67
- lizard-1.17.31.dist-info/entry_points.txt,sha256=ZBqPhu-J3NoGGW5vn2Gfyoo0vdVlgBgM-wlNm0SGYUQ,39
68
- lizard-1.17.31.dist-info/top_level.txt,sha256=5NTrTaOLhHuTzXaGcZPKfuaOgUv7WafNGe0Zl5aycpg,35
69
- lizard-1.17.31.dist-info/RECORD,,
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,,
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  lizard = lizard:main
3
+
lizard.py CHANGED
@@ -623,7 +623,7 @@ def whitelist_filter(warnings, script=None, whitelist=None):
623
623
 
624
624
  def get_whitelist(whitelist):
625
625
  if os.path.isfile(whitelist):
626
- return open(whitelist, mode='r').read()
626
+ return auto_read(whitelist)
627
627
  if whitelist != DEFAULT_WHITELIST:
628
628
  print("WARNING: !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
629
629
  print("WARNING: the whitelist \""+whitelist+"\" doesn't exist.")
@@ -923,12 +923,12 @@ def get_all_source_files(paths, exclude_patterns, lans):
923
923
  for path in paths:
924
924
  gitignore_path = os.path.join(path, '.gitignore')
925
925
  if os.path.exists(gitignore_path):
926
- with open(gitignore_path, 'r') as gitignore_file:
927
- # Read lines and strip whitespace and empty lines
928
- patterns = [line.strip() for line in gitignore_file.readlines()]
929
- patterns = [p for p in patterns if p and not p.startswith('#')]
930
- gitignore_spec = pathspec.PathSpec.from_lines('gitwildmatch', patterns)
931
- base_path = path
926
+ gitignore_file = auto_read(gitignore_path)
927
+ # Read lines and strip whitespace and empty lines
928
+ patterns = [line.strip() for line in gitignore_file.splitlines()]
929
+ patterns = [p for p in patterns if p and not p.startswith('#')]
930
+ gitignore_spec = pathspec.PathSpec.from_lines('gitwildmatch', patterns)
931
+ base_path = path
932
932
  break
933
933
  except ImportError:
934
934
  pass
@@ -2,6 +2,7 @@
2
2
  Checkstyle XML output for Lizard
3
3
  '''
4
4
 
5
+
5
6
  def checkstyle_output(all_result, verbose):
6
7
  result = all_result.result
7
8
  import xml.etree.ElementTree as ET
@@ -20,7 +21,8 @@ def checkstyle_output(all_result, verbose):
20
21
  line=str(func.start_line),
21
22
  column="0",
22
23
  severity="info",
23
- message=f"{func.name} has {func.nloc} NLOC, {func.cyclomatic_complexity} CCN, {func.token_count} token, {len(func.parameters)} PARAM, {func.length} length",
24
+ message=f"{func.name} has {func.nloc} NLOC, {func.cyclomatic_complexity} CCN, "
25
+ f"{func.token_count} token, {len(func.parameters)} PARAM, {func.length} length",
24
26
  source="lizard"
25
27
  )
26
28
 
@@ -28,4 +30,4 @@ def checkstyle_output(all_result, verbose):
28
30
  import xml.dom.minidom
29
31
  rough_string = ET.tostring(checkstyle, 'utf-8')
30
32
  reparsed = xml.dom.minidom.parseString(rough_string)
31
- return reparsed.toprettyxml(indent=" ")
33
+ return reparsed.toprettyxml(indent=" ")
lizard_ext/version.py CHANGED
@@ -3,4 +3,4 @@
3
3
  #
4
4
  # pylint: disable=missing-docstring,invalid-name
5
5
 
6
- version = "1.17.31"
6
+ version = "1.18.0"
@@ -21,10 +21,11 @@ from .rust import RustReader
21
21
  from .typescript import TypeScriptReader
22
22
  from .fortran import FortranReader
23
23
  from .solidity import SolidityReader
24
- from .jsx import JSXReader
25
24
  from .tsx import TSXReader
26
25
  from .vue import VueReader
27
26
  from .perl import PerlReader
27
+ from .st import StReader
28
+ from .r import RReader
28
29
 
29
30
 
30
31
  def languages():
@@ -50,10 +51,11 @@ def languages():
50
51
  SolidityReader,
51
52
  ErlangReader,
52
53
  ZigReader,
53
- JSXReader,
54
54
  TSXReader,
55
55
  VueReader,
56
56
  PerlReader,
57
+ StReader,
58
+ RReader,
57
59
  ]
58
60
 
59
61
 
lizard_languages/clike.py CHANGED
@@ -19,7 +19,7 @@ class CCppCommentsMixin(object): # pylint: disable=R0903
19
19
  class CLikeReader(CodeReader, CCppCommentsMixin):
20
20
  ''' This is the reader for C, C++ and Java. '''
21
21
 
22
- ext = ["c", "cpp", "cc", "mm", "cxx", "h", "hpp"]
22
+ ext = ["c", "cpp", "cc", "cxx", "h", "hpp"]
23
23
  language_names = ['cpp', 'c']
24
24
  macro_pattern = re.compile(r"#\s*(\w+)\s*(.*)", re.M | re.S)
25
25
 
@@ -162,6 +162,11 @@ class CLikeStates(CodeStateMachine):
162
162
  def _state_global(self, token):
163
163
  if token[0].isalpha() or token[0] in '_~':
164
164
  self.try_new_function(token)
165
+ elif token == '[':
166
+ # Check if this might be a lambda expression (C++ only)
167
+ # Java doesn't have lambda expressions, so skip lambda detection for Java
168
+ if not hasattr(self, 'class_name'): # JavaStates has class_name attribute
169
+ self._state = self._state_lambda_check
165
170
 
166
171
  def _state_function(self, token):
167
172
  if token == '(':
@@ -310,3 +315,57 @@ class CLikeStates(CodeStateMachine):
310
315
  def _state_attribute(self, _):
311
316
  "Ignores function attributes with C++11 syntax, i.e., [[ attribute ]]."
312
317
  pass
318
+
319
+ def _state_lambda_check(self, token):
320
+ """Check if this is a lambda expression or a function attribute."""
321
+ if token == ']':
322
+ # This is a lambda expression [](params) or [capture](params)
323
+ # Skip the lambda and continue parsing normally
324
+ self._state = self._state_lambda_params
325
+ elif token == '[':
326
+ # This is a function attribute [[attribute]]
327
+ self._state = self._state_attribute
328
+ else:
329
+ # This is a lambda with capture list [capture](params)
330
+ # Skip until we find the closing bracket
331
+ self._state = self._state_lambda_capture
332
+
333
+ def _state_lambda_params(self, token):
334
+ """Handle lambda parameters and body."""
335
+ if token == '(':
336
+ # Start of parameter list, skip until closing parenthesis
337
+ self._state = self._state_lambda_param_list
338
+ else:
339
+ # No parameters, check for body
340
+ self._state = self._state_lambda_body
341
+
342
+ def _state_lambda_param_list(self, token):
343
+ """Handle lambda parameter list."""
344
+ if token == ')':
345
+ # End of parameter list, check for body
346
+ self._state = self._state_lambda_body
347
+ # Otherwise, continue in parameter list
348
+
349
+ def _state_lambda_body(self, token):
350
+ """Handle lambda body."""
351
+ if token == '{':
352
+ # Start of lambda body, skip until closing brace
353
+ self._state = self._state_lambda_body_skip
354
+ elif token == ';':
355
+ # Lambda without body, just a semicolon
356
+ self._state = self._state_global
357
+ # Otherwise, continue
358
+
359
+ def _state_lambda_body_skip(self, token):
360
+ """Skip lambda body until closing brace."""
361
+ if token == '}':
362
+ # End of lambda body, continue parsing normally
363
+ self._state = self._state_global
364
+ # Otherwise, continue skipping
365
+
366
+ def _state_lambda_capture(self, token):
367
+ """Handle lambda capture list."""
368
+ if token == ']':
369
+ # End of capture list, continue parsing normally
370
+ self._state = self._state_global
371
+ # Otherwise, continue in capture list
@@ -37,6 +37,7 @@ class PythonReader(CodeReader, ScriptLanguageMixIn):
37
37
  def __init__(self, context):
38
38
  super(PythonReader, self).__init__(context)
39
39
  self.parallel_states = [PythonStates(context, self)]
40
+ self._last_meaningful_token = None # Track the last meaningful token
40
41
 
41
42
  @staticmethod
42
43
  def generate_tokens(source_code, addition='', token_class=None):
@@ -46,6 +47,41 @@ class PythonReader(CodeReader, ScriptLanguageMixIn):
46
47
  r"|(?:\'\'\'(?:\\.|[^\']|\'(?!\'\')|\'\'(?!\'))*\'\'\')",
47
48
  token_class)
48
49
 
50
+ def process_token(self, token):
51
+ """Process triple-quoted strings used as comments.
52
+
53
+ Triple-quoted strings that are not docstrings (i.e., not immediately
54
+ after function definitions) should be treated like comments and not
55
+ counted in NLOC, but only if they appear to be standalone statements
56
+ rather than part of assignments or other expressions.
57
+
58
+ Returns:
59
+ bool: True if the token was handled specially, False otherwise
60
+ """
61
+ if (token.startswith('"""') or token.startswith("'''")) and len(token) >= 6:
62
+ # Check if this is likely a standalone comment (not a docstring)
63
+ # Docstrings are handled separately in _state_first_line
64
+ current_state = self.parallel_states[0]._state
65
+
66
+ # If we're not in the first line state, check if this is a standalone string
67
+ if current_state != current_state.__self__._state_first_line:
68
+ # Check if the immediate previous meaningful token suggests this is part of an expression
69
+ assignment_tokens = ['=', '+=', '-=', '*=', '/=', '%=', '//=', '**=', '&=', '|=', '^=',
70
+ '<<=', '>>=', '(', 'return', ',', '[', '+', '-', '*', '/', '%']
71
+
72
+ is_part_of_expression = self._last_meaningful_token in assignment_tokens
73
+
74
+ # Only treat as comment if it's NOT part of an expression
75
+ if not is_part_of_expression:
76
+ # Subtract the NLOC contribution of this triple-quoted string
77
+ self.context.add_nloc(-(token.count('\n') + 1))
78
+
79
+ # Update last meaningful token (ignore whitespace and newlines)
80
+ if token not in ['\n', ' ', '\t'] and not token.isspace():
81
+ self._last_meaningful_token = token
82
+
83
+ return False # Continue with normal processing
84
+
49
85
  def preprocess(self, tokens):
50
86
  indents = PythonIndents(self.context)
51
87
  current_leading_spaces = 0
lizard_languages/r.py ADDED
@@ -0,0 +1,290 @@
1
+ '''
2
+ Language parser for R
3
+ '''
4
+
5
+ from .code_reader import CodeReader, CodeStateMachine
6
+ from .script_language import ScriptLanguageMixIn
7
+
8
+
9
+ class RReader(CodeReader, ScriptLanguageMixIn):
10
+ """R language reader for parsing R code and calculating complexity metrics."""
11
+
12
+ ext = ['r', 'R']
13
+ language_names = ['r', 'R']
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
+ }
21
+
22
+ def __init__(self, context):
23
+ super(RReader, self).__init__(context)
24
+ self.parallel_states = [RStates(context)]
25
+
26
+ def preprocess(self, tokens):
27
+ """Preprocess tokens - for now just pass them through."""
28
+ for token in tokens:
29
+ yield token
30
+
31
+ @staticmethod
32
+ def generate_tokens(source_code, addition='', token_class=None):
33
+ """Generate tokens for R code with R-specific patterns."""
34
+ # R-specific token patterns
35
+ r_patterns = (
36
+ r"|<-" # Assignment operator <-
37
+ r"|->" # Assignment operator ->
38
+ r"|%[a-zA-Z_*/>]+%" # Special operators like %in%, %*%, %>%, %/%, etc.
39
+ r"|\.\.\." # Ellipsis for variable arguments
40
+ r"|:::" # Internal namespace operator (must come before ::)
41
+ r"|::" # Namespace operator
42
+ )
43
+
44
+ return ScriptLanguageMixIn.generate_common_tokens(
45
+ source_code,
46
+ r_patterns + addition,
47
+ token_class
48
+ )
49
+
50
+
51
+ class RStates(CodeStateMachine):
52
+ """State machine for parsing R function definitions and complexity."""
53
+
54
+ def __init__(self, context):
55
+ super(RStates, self).__init__(context)
56
+ self.recent_tokens = [] # Track recent tokens to find function names
57
+ self.brace_count = 0 # Track brace nesting for function bodies
58
+ self.in_braced_function = False # Track if current function uses braces
59
+ self.additional_function_names = [] # Store additional names for multiple assignment
60
+
61
+ def _state_global(self, token):
62
+ """Global state - looking for function definitions."""
63
+ # Track recent non-whitespace tokens
64
+ if not token.isspace() and token != '\n':
65
+ self.recent_tokens.append(token)
66
+ if len(self.recent_tokens) > 10: # Keep only last 10 tokens
67
+ self.recent_tokens.pop(0)
68
+
69
+ # Look for function keyword after assignment operators
70
+ if token == 'function':
71
+ # Check if we have recent tokens: [name, assignment_op, 'function']
72
+ if len(self.recent_tokens) >= 2:
73
+ # recent_tokens now contains [..., assignment_op, 'function']
74
+ assignment_op = self.recent_tokens[-2] # The token before 'function'
75
+ if assignment_op in ['<-', '=']:
76
+ # Handle multiple assignments by creating separate functions
77
+ func_names = self._extract_function_names()
78
+
79
+ # Create the first function (this will be the main one with the function body)
80
+ self._start_function(func_names[0])
81
+ self._state = self._function_params
82
+
83
+ # Store additional names for later processing
84
+ self.additional_function_names = func_names[1:] if len(func_names) > 1 else []
85
+ return
86
+
87
+ # If we get here, it's an anonymous function or not a proper assignment
88
+ self._start_function("(anonymous)")
89
+ self._state = self._function_params
90
+
91
+ def _extract_function_names(self):
92
+ """Extract all function names from recent tokens, handling multiple assignments."""
93
+ if len(self.recent_tokens) < 3:
94
+ return ["(anonymous)"]
95
+
96
+ # Look backwards from the assignment operator to find all function names
97
+ # For multiple assignment like: a <- b <- c <- function(...)
98
+ # recent_tokens ends with [..., 'a', '<-', 'b', '<-', 'c', '<-', 'function']
99
+ assignment_index = len(self.recent_tokens) - 2 # Position of assignment operator
100
+
101
+ function_names = []
102
+ i = assignment_index - 1 # Start from token before assignment operator
103
+ current_name_tokens = []
104
+
105
+ while i >= 0:
106
+ token = self.recent_tokens[i]
107
+
108
+ # If we hit an assignment operator, we've found a complete variable name
109
+ if token in ['<-', '=']:
110
+ if current_name_tokens:
111
+ function_names.append(''.join(reversed(current_name_tokens)))
112
+ current_name_tokens = []
113
+ i -= 1
114
+ continue
115
+
116
+ # Stop if we hit keywords or operators that shouldn't be part of function names
117
+ if token in ['function', '(', ')', '{', '}', '\n']:
118
+ break
119
+
120
+ # Valid R identifier characters and dots
121
+ if token.replace('_', 'a').replace('.', 'a').isalnum() or token == '.':
122
+ current_name_tokens.append(token)
123
+ i -= 1
124
+ else:
125
+ break
126
+
127
+ # Add the last name if we have one
128
+ if current_name_tokens:
129
+ function_names.append(''.join(reversed(current_name_tokens)))
130
+
131
+ # Return names in the correct order (left to right as they appear in code)
132
+ return list(reversed(function_names)) if function_names else ["(anonymous)"]
133
+
134
+ def _extract_function_name(self):
135
+ """Extract the first function name (for backward compatibility)."""
136
+ names = self._extract_function_names()
137
+ return names[0] if names else "(anonymous)"
138
+
139
+ def _start_function(self, name):
140
+ """Start tracking a new function."""
141
+ self.context.restart_new_function(name)
142
+
143
+ def _function_params(self, token):
144
+ """Expecting function parameters."""
145
+ if token == '(':
146
+ self.context.add_to_long_function_name("(")
147
+ self._state = self._read_params
148
+ else:
149
+ # Single expression function without parentheses - rare in R
150
+ self._state = self._function_body
151
+ self._function_body(token)
152
+
153
+ def _read_params(self, token):
154
+ """Read function parameters until closing parenthesis."""
155
+ if token == ')':
156
+ self.context.add_to_long_function_name(")")
157
+ self._state = self._function_body
158
+ elif token not in ['\n'] and not token.isspace():
159
+ self.context.parameter(token)
160
+ if token != '(':
161
+ self.context.add_to_long_function_name(" " + token)
162
+
163
+ def _function_body(self, token):
164
+ """In function body - track complexity and nested functions."""
165
+ # Note: Complexity conditions are automatically counted by the framework
166
+ # based on reader.conditions, so we don't need to manually count them here
167
+
168
+ # Continue tracking tokens even in function body for nested function detection
169
+ if not token.isspace() and token != '\n':
170
+ self.recent_tokens.append(token)
171
+ if len(self.recent_tokens) > 10: # Keep only last 10 tokens
172
+ self.recent_tokens.pop(0)
173
+
174
+ # Track braces
175
+ if token == '{':
176
+ if self.brace_count == 0:
177
+ self.in_braced_function = True
178
+ self.brace_count += 1
179
+ elif token == '}':
180
+ self.brace_count -= 1
181
+ if self.brace_count == 0 and self.in_braced_function:
182
+ # End of braced function
183
+ self._end_current_function()
184
+ return
185
+
186
+ # Handle nested functions - treat them as separate functions
187
+ if token == 'function':
188
+ # Check if this is a nested function assignment
189
+ if len(self.recent_tokens) >= 2:
190
+ assignment_op = self.recent_tokens[-2] # The token before 'function'
191
+ if assignment_op in ['<-', '=']:
192
+ # End current function first
193
+ self.context.end_of_function()
194
+
195
+ # Handle multiple assignments for nested functions too
196
+ func_names = self._extract_function_names()
197
+
198
+ # Start a new function for the nested function
199
+ self._start_function(func_names[0])
200
+ self._state = self._function_params
201
+ # Reset brace counting for the new function
202
+ self.brace_count = 0
203
+ self.in_braced_function = False
204
+
205
+ # Store additional names for later processing
206
+ self.additional_function_names = func_names[1:] if len(func_names) > 1 else []
207
+ return
208
+
209
+ # For single-line functions without braces, end at newline
210
+ elif token == '\n' and not self.in_braced_function:
211
+ self._end_current_function()
212
+
213
+ def _end_current_function(self):
214
+ """End the current function and reset state."""
215
+ # Check if this might be a right assignment case
216
+ # We need to temporarily not end the function to see if there's a right assignment
217
+ self._state = self._check_right_assignment
218
+ self.brace_count = 0
219
+ self.in_braced_function = False
220
+
221
+ def _check_right_assignment(self, token):
222
+ """Check if there's a right assignment after function end."""
223
+ # Skip whitespace and comments
224
+ if token.isspace() or token == '\n' or token.startswith('#'):
225
+ return
226
+
227
+ # Look for right assignment operator
228
+ if token == '->':
229
+ self._state = self._read_right_assignment_name
230
+ return
231
+
232
+ # If we encounter anything else, this is not a right assignment
233
+ # End the function and create additional functions for multiple assignments
234
+ self._finalize_function_with_multiple_assignments()
235
+ self._state = self._state_global
236
+ self._state_global(token)
237
+
238
+ def _finalize_function_with_multiple_assignments(self):
239
+ """End the current function and create additional functions for multiple assignments."""
240
+ # Get the current function's information before ending it
241
+ current_func = self.context.current_function
242
+
243
+ # End the current function
244
+ self.context.end_of_function()
245
+
246
+ # Create additional function entries for multiple assignments
247
+ if self.additional_function_names and current_func:
248
+ for func_name in self.additional_function_names:
249
+ # Create a new function with the same complexity and line info
250
+ self.context.restart_new_function(func_name)
251
+ # Copy the complexity from the original function
252
+ if hasattr(current_func, 'cyclomatic_complexity'):
253
+ self.context.current_function.cyclomatic_complexity = current_func.cyclomatic_complexity
254
+ # Set the same line range
255
+ self.context.current_function.start_line = current_func.start_line
256
+ self.context.current_function.end_line = current_func.end_line
257
+ # End this function immediately
258
+ self.context.end_of_function()
259
+
260
+ # Clear the additional names
261
+ self.additional_function_names = []
262
+
263
+ def _read_right_assignment_name(self, token):
264
+ """Read the function name after right assignment operator."""
265
+ # Skip whitespace
266
+ if token.isspace() or token == '\n':
267
+ return
268
+
269
+ # This should be the function name
270
+ if token.replace('_', 'a').replace('.', 'a').isalnum() or token == '.':
271
+ # Update the current function's name
272
+ if self.context.current_function:
273
+ self.context.current_function.name = token
274
+
275
+ # End the function and create additional functions for multiple assignments
276
+ self._finalize_function_with_multiple_assignments()
277
+ self._state = self._state_global
278
+ return
279
+
280
+ # If we get something unexpected, treat as anonymous function
281
+ self._finalize_function_with_multiple_assignments()
282
+ self._state = self._state_global
283
+ self._state_global(token)
284
+
285
+ def statemachine_before_return(self):
286
+ """Called when processing is complete - end any open functions."""
287
+ if self._state in [self._function_body, self._check_right_assignment, self._read_right_assignment_name]:
288
+ # End any open function and process multiple assignments
289
+ if hasattr(self.context, 'current_function') and self.context.current_function:
290
+ self._finalize_function_with_multiple_assignments()
lizard_languages/rust.py CHANGED
@@ -19,6 +19,11 @@ class RustReader(CodeReader, CCppCommentsMixin):
19
19
  super().__init__(context)
20
20
  self.parallel_states = [RustStates(context)]
21
21
 
22
+ @staticmethod
23
+ def generate_tokens(source_code, addition='', token_class=None):
24
+ addition = r"|(?:'\w+\b)" # lifetimes, labels
25
+ return CodeReader.generate_tokens(source_code, addition, token_class)
26
+
22
27
 
23
28
  class RustStates(GoLikeStates): # pylint: disable=R0903
24
29
  FUNC_KEYWORD = 'fn'