lizard 1.19.0__tar.gz → 1.20.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. {lizard-1.19.0 → lizard-1.20.0}/PKG-INFO +2 -2
  2. {lizard-1.19.0 → lizard-1.20.0}/lizard.egg-info/PKG-INFO +2 -2
  3. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/__init__.py +0 -1
  4. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardcomplextags.py +6 -0
  5. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardmccabe.py +6 -1
  6. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardmodified.py +7 -2
  7. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardnonstrict.py +2 -1
  8. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardns.py +31 -8
  9. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/version.py +1 -1
  10. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/clike.py +5 -2
  11. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/code_reader.py +26 -2
  12. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/csharp.py +21 -2
  13. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/erlang.py +9 -1
  14. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/fortran.py +5 -6
  15. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/gdscript.py +6 -2
  16. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/kotlin.py +6 -3
  17. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/perl.py +8 -4
  18. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/php.py +6 -2
  19. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/plsql.py +5 -2
  20. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/python.py +6 -4
  21. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/r.py +9 -6
  22. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/rubylike.py +6 -3
  23. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/rust.py +7 -2
  24. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/scala.py +6 -2
  25. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/solidity.py +6 -1
  26. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/st.py +9 -5
  27. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/swift.py +6 -2
  28. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/tnsdl.py +6 -1
  29. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/tsx.py +2 -2
  30. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/ttcn.py +5 -3
  31. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/typescript.py +6 -2
  32. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/zig.py +7 -1
  33. {lizard-1.19.0 → lizard-1.20.0}/test/testNestedStructures.py +55 -0
  34. {lizard-1.19.0 → lizard-1.20.0}/LICENSE.txt +0 -0
  35. {lizard-1.19.0 → lizard-1.20.0}/README.rst +0 -0
  36. {lizard-1.19.0 → lizard-1.20.0}/lizard.egg-info/SOURCES.txt +0 -0
  37. {lizard-1.19.0 → lizard-1.20.0}/lizard.egg-info/dependency_links.txt +0 -0
  38. {lizard-1.19.0 → lizard-1.20.0}/lizard.egg-info/entry_points.txt +0 -0
  39. {lizard-1.19.0 → lizard-1.20.0}/lizard.egg-info/requires.txt +0 -0
  40. {lizard-1.19.0 → lizard-1.20.0}/lizard.egg-info/top_level.txt +0 -0
  41. {lizard-1.19.0 → lizard-1.20.0}/lizard.py +0 -0
  42. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/auto_open.py +0 -0
  43. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/checkstyleoutput.py +0 -0
  44. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/csvoutput.py +0 -0
  45. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/default_ordered_dict.py +0 -0
  46. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/extension_base.py +0 -0
  47. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/htmloutput.py +0 -0
  48. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/keywords.py +0 -0
  49. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardboolcount.py +0 -0
  50. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardcpre.py +0 -0
  51. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizarddependencycount.py +0 -0
  52. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizarddumpcomments.py +0 -0
  53. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardduplicate.py +0 -0
  54. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardduplicated_param_list.py +0 -0
  55. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardexitcount.py +0 -0
  56. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardgotocount.py +0 -0
  57. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardignoreassert.py +0 -0
  58. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardio.py +0 -0
  59. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardnd.py +0 -0
  60. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardoutside.py +0 -0
  61. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardstatementcount.py +0 -0
  62. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/lizardwordcount.py +0 -0
  63. {lizard-1.19.0 → lizard-1.20.0}/lizard_ext/xmloutput.py +0 -0
  64. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/__init__.py +0 -0
  65. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/go.py +0 -0
  66. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/golike.py +0 -0
  67. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/java.py +0 -0
  68. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/javascript.py +0 -0
  69. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/js_style_regex_expression.py +0 -0
  70. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/lua.py +0 -0
  71. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/objc.py +0 -0
  72. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/ruby.py +0 -0
  73. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/script_language.py +0 -0
  74. {lizard-1.19.0 → lizard-1.20.0}/lizard_languages/vue.py +0 -0
  75. {lizard-1.19.0 → lizard-1.20.0}/setup.cfg +0 -0
  76. {lizard-1.19.0 → lizard-1.20.0}/setup.py +0 -0
  77. {lizard-1.19.0 → lizard-1.20.0}/test/testApplication.py +0 -0
  78. {lizard-1.19.0 → lizard-1.20.0}/test/testAssertionExtension.py +0 -0
  79. {lizard-1.19.0 → lizard-1.20.0}/test/testBasicFunctionInfo.py +0 -0
  80. {lizard-1.19.0 → lizard-1.20.0}/test/testCOutsideComplexity.py +0 -0
  81. {lizard-1.19.0 → lizard-1.20.0}/test/testCPreprocessorExtension.py +0 -0
  82. {lizard-1.19.0 → lizard-1.20.0}/test/testCommentOptions.py +0 -0
  83. {lizard-1.19.0 → lizard-1.20.0}/test/testCyclomaticComplexity.py +0 -0
  84. {lizard-1.19.0 → lizard-1.20.0}/test/testExtension.py +0 -0
  85. {lizard-1.19.0 → lizard-1.20.0}/test/testFilesFilter.py +0 -0
  86. {lizard-1.19.0 → lizard-1.20.0}/test/testFunctionDependencyCount.py +0 -0
  87. {lizard-1.19.0 → lizard-1.20.0}/test/testFunctionExitCount.py +0 -0
  88. {lizard-1.19.0 → lizard-1.20.0}/test/testFunctionGotoCount.py +0 -0
  89. {lizard-1.19.0 → lizard-1.20.0}/test/testFunctionStatementCount.py +0 -0
  90. {lizard-1.19.0 → lizard-1.20.0}/test/testHelpers.py +0 -0
  91. {lizard-1.19.0 → lizard-1.20.0}/test/testLanguages.py +0 -0
  92. {lizard-1.19.0 → lizard-1.20.0}/test/testMcCabe.py +0 -0
  93. {lizard-1.19.0 → lizard-1.20.0}/test/testNestingDepth.py +0 -0
  94. {lizard-1.19.0 → lizard-1.20.0}/test/testOutput.py +0 -0
  95. {lizard-1.19.0 → lizard-1.20.0}/test/testOutputCSV.py +0 -0
  96. {lizard-1.19.0 → lizard-1.20.0}/test/testOutputFile.py +0 -0
  97. {lizard-1.19.0 → lizard-1.20.0}/test/testOutputHTML.py +0 -0
  98. {lizard-1.19.0 → lizard-1.20.0}/test/testTokenizer.py +0 -0
  99. {lizard-1.19.0 → lizard-1.20.0}/test/test_analyzer.py +0 -0
  100. {lizard-1.19.0 → lizard-1.20.0}/test/test_auto_open.py +0 -0
  101. {lizard-1.19.0 → lizard-1.20.0}/test/test_options.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lizard
3
- Version: 1.19.0
3
+ Version: 1.20.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
- Download-URL: https://pypi.python.org/lizard/
7
6
  Author: Terry Yin
8
7
  Author-email: terry@odd-e.com
9
8
  License: MIT
9
+ Download-URL: https://pypi.python.org/lizard/
10
10
  Project-URL: Source, https://github.com/terryyin/lizard
11
11
  Platform: any
12
12
  Classifier: Development Status :: 5 - Production/Stable
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lizard
3
- Version: 1.19.0
3
+ Version: 1.20.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
- Download-URL: https://pypi.python.org/lizard/
7
6
  Author: Terry Yin
8
7
  Author-email: terry@odd-e.com
9
8
  License: MIT
9
+ Download-URL: https://pypi.python.org/lizard/
10
10
  Project-URL: Source, https://github.com/terryyin/lizard
11
11
  Platform: any
12
12
  Classifier: Development Status :: 5 - Production/Stable
@@ -21,7 +21,6 @@ def print_csv(results, options, _, total_factory):
21
21
 
22
22
  def print_checkstyle(results, options, _, total_factory, file=None):
23
23
  import sys
24
- print("DEBUG: print_checkstyle called", file=sys.stderr)
25
24
  output = checkstyle_output(total_factory(list(results)), options.verbose)
26
25
  if file is None:
27
26
  file = sys.stdout
@@ -5,10 +5,16 @@ that adding the complexity and the line numbers of the keywords appear.
5
5
 
6
6
 
7
7
  class LizardExtension(object): # pylint: disable=R0903
8
+ """
9
+ Complex tags extension: records all complexity-adding keywords and their line numbers.
10
+ Uses reader.conditions (combined set of all condition types) to track all
11
+ complexity contributors: control flow, logical operators, case labels, and ternary.
12
+ """
8
13
 
9
14
  # pylint: disable=W0221
10
15
  def __call__(self, tokens, reader):
11
16
  context = reader.context
17
+ # Use combined conditions set - intentionally includes all types
12
18
  conditions = reader.conditions
13
19
  for token in tokens:
14
20
  yield token
@@ -14,9 +14,14 @@ from .extension_base import ExtensionBase
14
14
 
15
15
 
16
16
  class LizardExtension(ExtensionBase): # pylint: disable=R0903
17
+ """
18
+ McCabe extension: only counts the first 'case' in a switch statement.
19
+ Consecutive cases without code between them don't add to complexity.
20
+ Works by detecting case tokens (conceptually from reader.case_keywords).
21
+ """
17
22
 
18
23
  def _state_global(self, token):
19
- if token == "case":
24
+ if token == "case": # Detect case keywords
20
25
  self._state = self._in_case
21
26
 
22
27
  def _in_case(self, token):
@@ -6,14 +6,19 @@ where the whole switch/case will be counted as 1.
6
6
 
7
7
 
8
8
  class LizardExtension(object): # pylint: disable=R0903
9
+ """
10
+ Modified CCN extension: counts entire switch/case as 1 complexity.
11
+ Adds +1 for 'switch', subtracts -1 for each 'case'.
12
+ Works with switch/case keywords (conceptually from reader.case_keywords).
13
+ """
9
14
 
10
15
  def __call__(self, tokens, reader):
11
16
  for token in tokens:
12
- if token == 'switch':
17
+ if token == 'switch': # Add complexity for switch statement
13
18
  reader.context.add_condition()
14
19
  if hasattr(reader.context, "add_nd_condition"):
15
20
  reader.context.add_nd_condition()
16
- elif token == 'case':
21
+ elif token == 'case': # Subtract complexity for each case
17
22
  reader.context.add_condition(-1)
18
23
  if hasattr(reader.context, "add_nd_condition"):
19
24
  reader.context.add_nd_condition(-1)
@@ -9,5 +9,6 @@ class LizardExtension(object): # pylint: disable=R0903
9
9
 
10
10
  # pylint: disable=W0221
11
11
  def __call__(self, tokens, reader):
12
- reader.conditions -= set(['&&', '||', 'and', 'or'])
12
+ # Remove logical operators from conditions (non-strict mode)
13
+ reader.conditions -= reader.logical_operators
13
14
  return tokens
@@ -67,10 +67,35 @@ class LizardExtension(ExtensionBase): # pylint: disable=R0903
67
67
 
68
68
  def __init__(self):
69
69
  super(LizardExtension, self).__init__(None)
70
- self.structure_piles = [0]
70
+ self.structure_piles = [0] # Invariant: must always have at least one element
71
71
 
72
- def pile_up_within_block(self):
72
+ def _push_scope(self):
73
+ """Push a new scope level. Safe to call anytime."""
74
+ self.structure_piles.append(0)
75
+
76
+ def _pop_scope(self):
77
+ """Pop a scope level. Maintains invariant of at least one element."""
78
+ if len(self.structure_piles) > 1:
79
+ self.structure_piles.pop()
80
+
81
+ def _increment_current_scope(self):
82
+ """Increment structure count in current scope. Safe even if piles corrupted."""
83
+ if not self.structure_piles:
84
+ self.structure_piles = [0] # Restore invariant
73
85
  self.structure_piles[-1] += 1
86
+
87
+ def _reset_or_decrement_current_scope(self, decrement=False):
88
+ """Reset or decrement current scope counter. Safe even if piles corrupted."""
89
+ if not self.structure_piles:
90
+ self.structure_piles = [0] # Restore invariant
91
+ return
92
+ if decrement:
93
+ self.structure_piles[-1] -= 1
94
+ else:
95
+ self.structure_piles[-1] = 0
96
+
97
+ def pile_up_within_block(self):
98
+ self._increment_current_scope()
74
99
  cur_level = sum(self.structure_piles)
75
100
  # Is there a path around _state_global?
76
101
  if not hasattr(self.context.current_function, "max_nested_structures"):
@@ -83,10 +108,10 @@ class LizardExtension(ExtensionBase): # pylint: disable=R0903
83
108
  if not hasattr(self.context.current_function, "max_nested_structures"):
84
109
  self.context.current_function.max_nested_structures = 0
85
110
  if token == '{':
86
- self.structure_piles.append(0)
111
+ self._push_scope()
87
112
  elif token in ';}':
88
113
  if token == '}':
89
- self.structure_piles.pop()
114
+ self._pop_scope()
90
115
  self._state = self._block_ending
91
116
  elif token in self.structures:
92
117
  self._state = self._in_structure_head
@@ -98,10 +123,8 @@ class LizardExtension(ExtensionBase): # pylint: disable=R0903
98
123
  self._state(token)
99
124
 
100
125
  def _block_ending(self, token):
101
- if token in self.matching_structures:
102
- self.structure_piles[-1] -= 1
103
- else:
104
- self.structure_piles[-1] = 0
126
+ decrement = token in self.matching_structures
127
+ self._reset_or_decrement_current_scope(decrement=decrement)
105
128
  self._state = self._state_global
106
129
  self._state(token)
107
130
 
@@ -3,4 +3,4 @@
3
3
  #
4
4
  # pylint: disable=missing-docstring,invalid-name
5
5
 
6
- version = "1.19.0"
6
+ version = "1.20.0"
@@ -32,8 +32,11 @@ class CLikeReader(CodeReader, CCppCommentsMixin):
32
32
 
33
33
  @staticmethod
34
34
  def generate_tokens(source_code, addition='', token_class=None):
35
- # Add pattern for floating point literals to the token generation
36
- addition = r"|(?:\d*\.\d+(?:[eE][-+]?\d+)?)" + \
35
+ # Add pattern for C++ raw string literals R"delimiter(content)delimiter"
36
+ # The delimiter can be empty or up to 16 chars (excluding parentheses, backslash, whitespace)
37
+ # Using a simplified pattern that handles most cases
38
+ addition = r"|R\"[^(\\]*\((?:[^)]|\)[^\"])*\)[^(\\]*\"" + \
39
+ r"|(?:\d*\.\d+(?:[eE][-+]?\d+)?)" + \
37
40
  r"|(?:\d+\.(?:\d+)?(?:[eE][-+]?\d+)?)" + \
38
41
  addition
39
42
  return CodeReader.generate_tokens(source_code, addition, token_class)
@@ -94,12 +94,36 @@ class CodeReader:
94
94
  ext = []
95
95
  languages = None
96
96
  extra_subclasses = set()
97
- _conditions = {'if', 'for', 'while', '&&', '||', '?', 'catch', 'case'}
97
+
98
+ # Condition categories - separate types that contribute to cyclomatic complexity
99
+ _control_flow_keywords = {'if', 'for', 'while', 'catch'}
100
+ _logical_operators = {'&&', '||'}
101
+ _case_keywords = {'case'}
102
+ _ternary_operators = {'?'}
103
+
104
+ @classmethod
105
+ def _build_conditions(cls):
106
+ """Build combined conditions set from separated categories.
107
+
108
+ Returns combined set of all condition types for CCN calculation.
109
+ """
110
+ return (cls._control_flow_keywords |
111
+ cls._logical_operators |
112
+ cls._case_keywords |
113
+ cls._ternary_operators)
98
114
 
99
115
  def __init__(self, context):
100
116
  self.parallel_states = []
101
117
  self.context = context
102
- self.conditions = copy(self._conditions)
118
+
119
+ # Build combined conditions set from separated categories
120
+ self.conditions = copy(self.__class__._build_conditions())
121
+
122
+ # Expose individual categories for extensions
123
+ self.control_flow_keywords = copy(self.__class__._control_flow_keywords)
124
+ self.logical_operators = copy(self.__class__._logical_operators)
125
+ self.case_keywords = copy(self.__class__._case_keywords)
126
+ self.ternary_operators = copy(self.__class__._ternary_operators)
103
127
 
104
128
  @classmethod
105
129
  def match_filename(cls, filename):
@@ -11,8 +11,11 @@ class CSharpReader(CLikeReader):
11
11
  ext = ['cs']
12
12
  language_names = ['csharp']
13
13
 
14
- _conditions = set(['if', 'for', 'while', '&&', '||', '?', 'catch',
15
- 'case', '??'])
14
+ # Separated condition categories
15
+ _control_flow_keywords = {'if', 'for', 'while', 'catch'}
16
+ _logical_operators = {'&&', '||'}
17
+ _case_keywords = {'case'}
18
+ _ternary_operators = {'?', '??'} # C# has both ?: and ?? (null-coalescing)
16
19
 
17
20
  def __init__(self, context):
18
21
  super(CSharpReader, self).__init__(context)
@@ -39,6 +42,22 @@ class CSharpStates(CLikeStates):
39
42
  if self.class_name and self.context.current_function:
40
43
  self.context.current_function.name = f"{self.class_name}::{name}"
41
44
 
45
+ def _state_dec_to_imp(self, token):
46
+ """Override to handle C# expression-bodied members (=>)"""
47
+ if token == '=>':
48
+ # Expression-bodied member: confirm function and enter expression body
49
+ self.context.confirm_new_function()
50
+ self._state = self._state_expression_body
51
+ else:
52
+ super(CSharpStates, self)._state_dec_to_imp(token)
53
+
54
+ def _state_expression_body(self, token):
55
+ """Read the expression body until semicolon, processing tokens for complexity"""
56
+ if token == ';':
57
+ # End of expression-bodied method, finalize function and return to global state
58
+ self.context.end_of_function()
59
+ self._state = self._state_global
60
+
42
61
  def _state_global(self, token):
43
62
  if token in ("class", "struct", "record"):
44
63
  self.class_name = None
@@ -13,7 +13,15 @@ class ErlangReader(CodeReader):
13
13
 
14
14
  ext = ['erl', 'hrl', 'es', 'escript']
15
15
  language_names = ['erlang']
16
- _conditions = {'and', 'case', 'catch', 'if', 'not', 'or', '?', 'when'}
16
+
17
+ # Separated condition categories
18
+ _control_flow_keywords = {'if', 'catch', 'when'} # when is used in guards
19
+ _logical_operators = {'and', 'or', 'not'}
20
+ _case_keywords = {'case'}
21
+ # Note: '?' in Erlang is a macro expansion operator (e.g., ?MODULE, ?EMPTY_NODE)
22
+ # Unlike C-style ternary, it's for compile-time macro substitution
23
+ # Included in ternary_operators because macro usage adds to code complexity
24
+ _ternary_operators = {'?'}
17
25
 
18
26
  def __init__(self, context):
19
27
  super(ErlangReader, self).__init__(context)
@@ -19,12 +19,11 @@ class FortranReader(CodeReader, FortranCommentsMixin):
19
19
  ext = ['f70', 'f90', 'f95', 'f03', 'f08', 'f', 'for', 'ftn', 'fpp']
20
20
  language_names = ['fortran']
21
21
 
22
- # Conditions need to have all the cases because the matching is case-insensitive
23
- # and is not done here.
24
- _conditions = {
25
- 'IF', 'DO', '.AND.', '.OR.', 'CASE',
26
- 'if', 'do', '.and.', '.or.', 'case'
27
- }
22
+ # Separated condition categories (case-insensitive language)
23
+ _control_flow_keywords = {'IF', 'DO', 'if', 'do'}
24
+ _logical_operators = {'.AND.', '.OR.', '.and.', '.or.'}
25
+ _case_keywords = {'CASE', 'case'}
26
+ _ternary_operators = set()
28
27
  _blocks = [
29
28
  'PROGRAM', 'MODULE', 'SUBMODULE', 'SUBROUTINE', 'FUNCTION', 'TYPE',
30
29
  'INTERFACE', 'BLOCK', 'IF', 'DO', 'FORALL', 'WHERE', 'SELECT', 'ASSOCIATE'
@@ -9,8 +9,12 @@ class GDScriptReader(PythonReader):
9
9
 
10
10
  ext = ['gd']
11
11
  language_names = ['GDScript']
12
- _conditions = set(['if', 'else', 'for', 'while', '&&', '||', '?', 'catch',
13
- 'case', 'do'])
12
+
13
+ # Separated condition categories
14
+ _control_flow_keywords = {'if', 'elif', 'for', 'while', 'catch', 'do'}
15
+ _logical_operators = {'&&', '||'}
16
+ _case_keywords = {'case'}
17
+ _ternary_operators = {'?'}
14
18
 
15
19
  def __init__(self, context):
16
20
  super(GDScriptReader, self).__init__(context)
@@ -13,9 +13,12 @@ class KotlinReader(CodeReader, CCppCommentsMixin, SwiftReplaceLabel):
13
13
 
14
14
  ext = ['kt', 'kts']
15
15
  language_names = ['kotlin']
16
- _conditions = {
17
- 'if', 'for', 'while', 'catch', '&&', '||', '?:'
18
- }
16
+
17
+ # Separated condition categories
18
+ _control_flow_keywords = {'if', 'for', 'while', 'catch'}
19
+ _logical_operators = {'&&', '||'}
20
+ _case_keywords = set() # Kotlin uses 'when' expressions, not case
21
+ _ternary_operators = {'?:'} # Elvis operator
19
22
 
20
23
  def __init__(self, context):
21
24
  super(KotlinReader, self).__init__(context)
@@ -19,8 +19,13 @@ class PerlReader(CodeReader, ScriptLanguageMixIn):
19
19
 
20
20
  ext = ['pl', 'pm']
21
21
  language_names = ['perl']
22
- _conditions = set(['if', 'elsif', 'unless', 'while', 'until', 'for', 'foreach',
23
- '&&', '||', '?', ':', 'when', 'given', 'default', 'do'])
22
+
23
+ # Separated condition categories
24
+ _control_flow_keywords = {'if', 'elsif', 'unless', 'while', 'until', 'for',
25
+ 'foreach', 'when', 'given', 'default', 'do'}
26
+ _logical_operators = {'&&', '||'} # Perl also has 'and', 'or' with different precedence
27
+ _case_keywords = set()
28
+ _ternary_operators = {'?', ':'} # Both parts of ternary operator
24
29
 
25
30
  def __init__(self, context):
26
31
  super(PerlReader, self).__init__(context)
@@ -65,8 +70,7 @@ class PerlReader(CodeReader, ScriptLanguageMixIn):
65
70
 
66
71
 
67
72
  class PerlStates(CodeStateMachine):
68
- _conditions = set(['if', 'elsif', 'unless', 'while', 'until', 'for', 'foreach',
69
- '&&', '||', '?', ':', 'when', 'given', 'default', 'do'])
73
+ # Note: _conditions removed - now inherited from PerlReader
70
74
 
71
75
  def __init__(self, context):
72
76
  super(PerlStates, self).__init__(context)
@@ -231,8 +231,12 @@ class PHPReader(CodeReader, CCppCommentsMixin):
231
231
 
232
232
  ext = ['php']
233
233
  language_names = ['php']
234
- _conditions = set(['if', 'elseif', 'for', 'foreach', 'while', '&&', '||', '?',
235
- 'catch', 'case', 'match'])
234
+
235
+ # Separated condition categories
236
+ _control_flow_keywords = {'if', 'elseif', 'for', 'foreach', 'while', 'catch', 'match'}
237
+ _logical_operators = {'&&', '||'} # PHP also has 'and', 'or' with different precedence
238
+ _case_keywords = {'case'}
239
+ _ternary_operators = {'?'}
236
240
 
237
241
  @staticmethod
238
242
  def generate_tokens(source_code, addition='', token_class=None):
@@ -45,11 +45,14 @@ class PLSQLReader(CodeReader, CCppCommentsMixin):
45
45
  ext = ["sql", "pks", "pkb", "pls", "plb", "pck"]
46
46
  language_names = ["plsql", "pl/sql"]
47
47
 
48
- # PL/SQL conditions for cyclomatic complexity
48
+ # Separated condition categories
49
49
  # Note: 'loop' is NOT in this set because LOOP has special handling:
50
50
  # - standalone LOOP adds +1
51
51
  # - LOOP after WHILE/FOR should not add (it's part of the compound statement)
52
- _conditions = {"if", "elsif", "when", "while", "for", "and", "or"}
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()
53
56
 
54
57
  def __init__(self, context):
55
58
  super(PLSQLReader, self).__init__(context)
@@ -29,10 +29,12 @@ class PythonReader(CodeReader, ScriptLanguageMixIn):
29
29
 
30
30
  ext = ['py']
31
31
  language_names = ['python']
32
- _conditions = set([
33
- 'if', 'for', 'while', 'and', 'or',
34
- 'elif', 'except', 'finally'
35
- ])
32
+
33
+ # Separated condition categories
34
+ _control_flow_keywords = {'if', 'elif', 'for', 'while', 'except', 'finally'}
35
+ _logical_operators = {'and', 'or'}
36
+ _case_keywords = set() # Python uses if/elif, not case
37
+ _ternary_operators = set() # Python uses 'x if c else y' syntax, not ?
36
38
 
37
39
  def __init__(self, context):
38
40
  super(PythonReader, self).__init__(context)
@@ -12,12 +12,15 @@ class RReader(CodeReader, ScriptLanguageMixIn):
12
12
  ext = ['r', 'R']
13
13
  language_names = ['r', 'R']
14
14
 
15
- # R-specific conditions that increase cyclomatic complexity
16
- _conditions = {
17
- 'if', 'else if', 'for', 'while', 'repeat', 'switch',
18
- '&&', '||', '&', '|', 'ifelse',
19
- 'tryCatch', 'try'
20
- }
15
+ # Separated condition categories
16
+ _control_flow_keywords = {'if', 'else if', 'for', 'while', 'repeat', 'switch',
17
+ 'tryCatch', 'try', 'ifelse'} # ifelse is a vectorized control function
18
+ # R has both short-circuit (&&, ||) and element-wise (&, |) operators
19
+ # Both types count toward CCN as they represent conditional logic (vectorized or not)
20
+ # Users can use -Enonstrict to exclude logical operators if desired
21
+ _logical_operators = {'&&', '||', '&', '|'}
22
+ _case_keywords = set()
23
+ _ternary_operators = set()
21
24
 
22
25
  def __init__(self, context):
23
26
  super(RReader, self).__init__(context)
@@ -97,9 +97,12 @@ class RubylikeStateMachine(CodeStateMachine):
97
97
  class RubylikeReader(CodeReader, ScriptLanguageMixIn):
98
98
  # pylint: disable=R0903
99
99
 
100
- _conditions = set(['if', 'until', 'for', 'while', 'and', 'or',
101
- 'elsif', 'elseif', 'rescue',
102
- 'ensure', 'when', '||', '&&', '?'])
100
+ # Separated condition categories
101
+ _control_flow_keywords = {'if', 'elsif', 'elseif', 'until', 'for', 'while',
102
+ 'rescue', 'ensure', 'when'}
103
+ _logical_operators = {'and', 'or', '||', '&&'} # Both word and symbol forms
104
+ _case_keywords = set() # Ruby uses 'when' for case expressions
105
+ _ternary_operators = {'?'}
103
106
 
104
107
  def __init__(self, context):
105
108
  super(RubylikeReader, self).__init__(context)
@@ -12,8 +12,13 @@ class RustReader(CodeReader, CCppCommentsMixin):
12
12
 
13
13
  ext = ['rs']
14
14
  language_names = ['rust']
15
- _conditions = set(['if', 'for', 'while', '&&', '||', '?', 'catch',
16
- 'case', 'match', 'where'])
15
+
16
+ # Separated condition categories
17
+ _control_flow_keywords = {'if', 'for', 'while', 'catch', 'match', 'where'}
18
+ _logical_operators = {'&&', '||'}
19
+ _case_keywords = set() # Rust uses match arms, not case keyword
20
+ # Note: '?' in Rust is the error propagation operator, not ternary
21
+ _ternary_operators = {'?'}
17
22
 
18
23
  def __init__(self, context):
19
24
  super().__init__(context)
@@ -12,8 +12,12 @@ class ScalaReader(CodeReader, CCppCommentsMixin):
12
12
 
13
13
  ext = ['scala']
14
14
  language_names = ['scala']
15
- _conditions = set(['if', 'for', 'while', '&&', '||', '?', 'catch',
16
- 'case', 'do'])
15
+
16
+ # Separated condition categories
17
+ _control_flow_keywords = {'if', 'for', 'while', 'catch', 'do'}
18
+ _logical_operators = {'&&', '||'}
19
+ _case_keywords = {'case'} # Pattern matching
20
+ _ternary_operators = {'?'}
17
21
 
18
22
  def __init__(self, context):
19
23
  super(ScalaReader, self).__init__(context)
@@ -11,7 +11,12 @@ class SolidityReader(CodeReader, CCppCommentsMixin):
11
11
 
12
12
  ext = ['sol']
13
13
  language_names = ['solidity']
14
- _conditions = set(['if', 'for', 'while', '&&', '||', '?'])
14
+
15
+ # Separated condition categories
16
+ _control_flow_keywords = {'if', 'for', 'while'}
17
+ _logical_operators = {'&&', '||'}
18
+ _case_keywords = set()
19
+ _ternary_operators = {'?'}
15
20
 
16
21
  def __init__(self, context):
17
22
  super(SolidityReader, self).__init__(context)
@@ -22,11 +22,15 @@ class StReader(CodeReader, StCommentsMixin):
22
22
  language_names = ['st']
23
23
  macro_pattern = re.compile(r"#\s*(\w+)\s*(.*)", re.M | re.S)
24
24
 
25
- # track block starters
26
- _conditions = set([
27
- 'if', 'elsif', 'case', 'for', 'while', 'repeat',
28
- 'IF', 'ELSIF', 'CASE', 'FOR', 'WHILE', 'REPEAT'
29
- ])
25
+ # Separated condition categories (case-insensitive language)
26
+ _control_flow_keywords = {
27
+ 'if', 'elsif', 'for', 'while', 'repeat',
28
+ 'IF', 'ELSIF', 'FOR', 'WHILE', 'REPEAT'
29
+ }
30
+ # ST is case-insensitive, so include both lowercase and uppercase forms
31
+ _logical_operators = {'and', 'or', 'AND', 'OR'}
32
+ _case_keywords = {'case', 'CASE'}
33
+ _ternary_operators = set()
30
34
 
31
35
  _functions = set([
32
36
  'FUNCTION_BLOCK', 'FUNCTION', 'ACTION'
@@ -30,8 +30,12 @@ class SwiftReader(CodeReader, CCppCommentsMixin, SwiftReplaceLabel):
30
30
  FUNC_KEYWORD = 'def'
31
31
  ext = ['swift']
32
32
  language_names = ['swift']
33
- _conditions = set(['if', 'for', 'while', '&&', '||', '?', 'catch',
34
- 'case', 'guard'])
33
+
34
+ # Separated condition categories
35
+ _control_flow_keywords = {'if', 'for', 'while', 'catch', 'guard'}
36
+ _logical_operators = {'&&', '||'}
37
+ _case_keywords = {'case'} # Pattern matching
38
+ _ternary_operators = {'?'}
35
39
 
36
40
  def __init__(self, context):
37
41
  super(SwiftReader, self).__init__(context)
@@ -84,7 +84,12 @@ class SDLReader(CodeReader, CCppCommentsMixin):
84
84
  if condition:
85
85
  return self.context.CONDITION()
86
86
 
87
- _conditions = set(['WHILE', 'AND', 'OR', '#if'])
87
+ # Separated condition categories
88
+ _control_flow_keywords = {'WHILE', '#if'}
89
+ _logical_operators = {'AND', 'OR'}
90
+ _case_keywords = set()
91
+ _ternary_operators = set()
92
+ # Note: No need to define _conditions - base class builds it automatically
88
93
 
89
94
  def _is_condition(self, token, last_token):
90
95
  if token == ':' and last_token == ')':
@@ -48,8 +48,8 @@ class JSXTypeScriptStates(CodeStateMachine):
48
48
  self.started_function = None
49
49
  self.pending_function_name = ''
50
50
  self._ts_declare = False
51
- self._conditions = set(['if', 'elseif', 'for', 'while', '&&', '||', '?',
52
- 'catch', 'case'])
51
+ # Note: Condition categories come from TypeScriptReader class level,
52
+ # no need to define instance-level duplicates
53
53
  self.inside_function = inside_function # Track if we're already inside a function
54
54
 
55
55
  def statemachine_before_return(self):
@@ -10,9 +10,11 @@ class TTCNReader(CLikeReader): # pylint: disable=R0903
10
10
  ext = ['ttcn', 'ttcnpp']
11
11
  language_names = ['ttcn', 'ttcn3']
12
12
 
13
- _conditions = set(['if', 'else', 'for', 'while',
14
- 'altstep', 'case', 'goto', 'alt',
15
- 'interleave', 'and', 'or', 'xor'])
13
+ # Separated condition categories
14
+ _control_flow_keywords = {'if', 'for', 'while', 'altstep', 'alt', 'interleave', 'goto'}
15
+ _logical_operators = {'and', 'or', 'xor'}
16
+ _case_keywords = {'case'}
17
+ _ternary_operators = set()
16
18
 
17
19
  def __init__(self, context):
18
20
  super(TTCNReader, self).__init__(context)
@@ -51,8 +51,12 @@ class TypeScriptReader(CodeReader, CCppCommentsMixin):
51
51
 
52
52
  ext = ['ts']
53
53
  language_names = ['typescript', 'ts']
54
- _conditions = set(['if', 'elseif', 'for', 'while', '&&', '||', '?',
55
- 'catch', 'case'])
54
+
55
+ # Separated condition categories
56
+ _control_flow_keywords = {'if', 'elseif', 'for', 'while', 'catch'}
57
+ _logical_operators = {'&&', '||'}
58
+ _case_keywords = {'case'}
59
+ _ternary_operators = {'?'}
56
60
 
57
61
  def __init__(self, context):
58
62
  super().__init__(context)
@@ -12,7 +12,13 @@ from .golike import GoLikeStates
12
12
  class ZigReader(CodeReader, CCppCommentsMixin):
13
13
  ext = ["zig"]
14
14
  language_names = ["zig"]
15
- _conditions = {"if", "for", "while", "and", "or", "orelse", "try", "catch", "=>"}
15
+
16
+ # Separated condition categories
17
+ _control_flow_keywords = {"if", "for", "while", "try", "catch"}
18
+ _logical_operators = {"and", "or", "orelse"} # orelse is Zig's null coalescing
19
+ _case_keywords = set()
20
+ # Note: '=>' is for error union and switch cases in Zig
21
+ _ternary_operators = {"=>"}
16
22
 
17
23
  def __init__(self, context):
18
24
  super().__init__(context)
@@ -375,6 +375,61 @@ class TestCppNestedStructures(unittest.TestCase):
375
375
  """)
376
376
  self.assertEqual(1, result[0].max_nested_structures)
377
377
 
378
+ def test_raw_string_literal_with_braces(self):
379
+ """Raw string literals containing braces should not cause IndexError."""
380
+ result = process_cpp(r'''
381
+ int main() {
382
+ const char* json = R"({
383
+ "name": "Ada Lovelace",
384
+ "id": 101,
385
+ "languages": ["C++", "Python", "Assembly"],
386
+ "active": true,
387
+ "profile": {
388
+ "bio": "Mathematician & pioneer",
389
+ "links": {
390
+ "website": "https://example.com/ada",
391
+ "github": "https://github.com/ada"
392
+ }
393
+ }
394
+ })";
395
+ for (int i = 1; i <= 3; ++i) {
396
+ for (int j = 1; j <= 3; ++j) {
397
+ return i + j;
398
+ }
399
+ }
400
+ return 0;
401
+ }
402
+ ''')
403
+ self.assertEqual(2, result[0].max_nested_structures)
404
+
405
+ def test_raw_string_literal_with_delimiter(self):
406
+ """Raw string literals with delimiters should be handled correctly."""
407
+ result = process_cpp(r'''
408
+ int fun() {
409
+ const char* s = R"delim(some {content} with )delim";
410
+ if (true) {
411
+ return 1;
412
+ }
413
+ return 0;
414
+ }
415
+ ''')
416
+ self.assertEqual(1, result[0].max_nested_structures)
417
+
418
+ def test_raw_string_literal_simple(self):
419
+ """Simple raw string literals should not interfere with complexity counting."""
420
+ result = process_cpp(r'''
421
+ int fun() {
422
+ const char* s = R"(hello world)";
423
+ if (a) {
424
+ if (b) {
425
+ return c;
426
+ }
427
+ }
428
+ return 0;
429
+ }
430
+ ''')
431
+ self.assertEqual(2, result[0].max_nested_structures)
432
+
378
433
 
379
434
  class X: #TestPythonNestedStructures(unittest.TestCase):
380
435
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes