Coverage for tests/test_cognitive/test_python.py: 100%

1"""Tests for Python cognitive complexity."""

2

3 from analyzers.python import analyze_source

4

5

6 def _score(code: str) -> int:

7 """Helper: return total complexity of the first function in code."""

8 result = analyze_source(code)

9 assert result.functions, f"No functions found in:\n{code}"

10 return result.functions[0].complexity

11

12

13 def _scores(code: str) -> dict[str, int]:

14 """Helper: return {name: complexity} for all functions in code."""

15 result = analyze_source(code)

16 return {f.name: f.complexity for f in result.functions}

17

18

19# --- Basic control flow ---

20

21

22 def test_simple_function():

23 assert _score("def f(): pass") == 0

24

25

26 def test_single_if():

27 code = """

28 def f(x):

29 if x: # +1

30 pass

31"""

32 assert _score(code) == 1

33

34

35 def test_if_else():

36 code = """

37 def f(x):

38 if x: # +1

39 pass

40 else: # +1

41 pass

42"""

43 assert _score(code) == 2

44

45

46 def test_if_elif_else():

47 code = """

48 def f(x):

49 if x > 0: # +1

50 pass

51 elif x < 0: # +1

52 pass

53 else: # +1

54 pass

55"""

56 assert _score(code) == 3

57

58

59 def test_for_loop():

60 code = """

61 def f(items):

62 for x in items: # +1

63 pass

64"""

65 assert _score(code) == 1

66

67

68 def test_while_loop():

69 code = """

70 def f(x):

71 while x > 0: # +1

72 x -= 1

73"""

74 assert _score(code) == 1

75

76

77# --- Nesting ---

78

79

80 def test_nested_if():

81 code = """

82 def f(a, b):

83 if a: # +1 (nesting 0)

84 if b: # +1 + 1 (nesting 1)

85 pass

86"""

87 assert _score(code) == 3

88

89

90 def test_deeply_nested():

91 code = """

92 def f(a, b, c):

93 if a: # +1 (nesting 0)

94 for x in b: # +1 + 1 (nesting 1)

95 if c: # +1 + 2 (nesting 2)

96 pass

97"""

98 assert _score(code) == 6

99

100

101 def test_if_else_nesting():

102 """else increases nesting for its body but gets no nesting bonus."""

103 code = """

104 def f(a, b):

105 if a: # +1 (nesting 0)

106 pass

107 else: # +1 (no nesting bonus)

108 if b: # +1 + 1 (nesting 1, inside else body)

109 pass

110"""

111 assert _score(code) == 4

112

113

114# --- Try/Except ---

115

116

117 def test_try_except():

118 """try has no increment; except gets +1."""

119 code = """

120 def f():

121 try:

122 pass

123 except ValueError: # +1

124 pass

125"""

126 assert _score(code) == 1

127

128

129 def test_try_except_nested():

130 code = """

131 def f(x):

132 if x: # +1 (nesting 0)

133 try:

134 pass

135 except ValueError: # +1 + 1 (nesting 1)

136 pass

137"""

138 assert _score(code) == 3

139

140

141# --- Boolean operators ---

142

143

144 def test_single_and():

145 code = """

146 def f(a, b):

147 if a and b: # +1 (if) + 1 (boolean)

148 pass

149"""

150 assert _score(code) == 2

151

152

153 def test_chained_same_op():

154 code = """

155 def f(a, b, c):

156 if a and b and c: # +1 (if) + 1 (boolean chain, same op)

157 pass

158"""

159 assert _score(code) == 2

160

161

162 def test_mixed_boolean():

163 code = """

164 def f(a, b, c):

165 if a and b or c: # +1 (if) + 2 (and->or switch)

166 pass

167"""

168 assert _score(code) == 3

169

170

171 def test_complex_boolean():

172 code = """

173 def f(a, b, c, d):

174 if a or b or c and d: # +1 (if) + 2 (or sequence, then switch to and)

175 pass

176"""

177 assert _score(code) == 3

178

179

180# --- Ternary / conditional expression ---

181

182

183 def test_ternary():

184 code = """

185 def f(x):

186 return x if x > 0 else -x # +1 (ternary, nesting 0)

187"""

188 assert _score(code) == 1

189

190

191 def test_ternary_nested():

192 code = """

193 def f(x):

194 if x: # +1 (nesting 0)

195 return x if x > 0 else -x # +1 + 1 (ternary, nesting 1)

196"""

197 assert _score(code) == 3

198

199

200# --- Recursion ---

201

202

203 def test_recursion():

204 code = """

205 def factorial(n):

206 if n <= 1: # +1

207 return 1

208 return n * factorial(n-1) # +1 (recursion)

209"""

210 assert _score(code) == 2

211

212

213 def test_no_false_recursion():

214 """Calling a different function shouldn't count as recursion."""

215 code = """

216 def f(x):

217 return g(x)

218"""

219 assert _score(code) == 0

220

221

222# --- Nested functions ---

223

224

225 def test_nested_function_separate_scoring():

226 """Nested functions are scored independently; outer doesn't include inner."""

227 code = """

228 def outer(x):

229 if x: # +1 for outer

230 pass

231 def inner(y):

232 if y: # +1 for inner

233 pass

234"""

235 scores = _scores(code)

236 assert scores["outer"] == 1

237 assert scores["inner"] == 1

238

239

240 def test_nested_function_nesting_reset():

241 """Nested function starts at nesting=0 regardless of outer nesting."""

242 code = """

243 def outer(x):

244 if x: # +1 for outer

245 def inner(y):

246 if y: # +1 for inner (nesting 0, not 1)

247 pass

248"""

249 scores = _scores(code)

250 assert scores["outer"] == 1

251 assert scores["inner"] == 1

252

253

254# --- Lambda ---

255

256

257 def test_lambda_nesting():

258 """Lambda increases nesting level."""

259 code = """

260 def f(items):

261 return list(map(lambda x: x if x > 0 else 0, items))

262"""

263 # lambda increases nesting to 1

264 # ternary inside lambda: +1 + 1 (nesting 1) = 2

265 assert _score(code) == 2

266

267

268# --- Comprehensive example ---

269

270

271 def test_comprehensive():

272 code = """

273 def process(items, flag):

274 if flag: # +1 (nesting 0)

275 for item in items: # +1 +1 (nesting 1)

276 if item > 0: # +1 +2 (nesting 2)

277 pass

278 elif item < 0: # +1 (no nesting bonus)

279 pass

280 else: # +1 (no nesting bonus)

281 try:

282 pass

283 except Exception: # +1 +3 (nesting 3)

284 pass

285 else: # +1 (no nesting bonus)

286 pass

287"""

288 # 1 + 2 + 3 + 1 + 1 + 4 + 1 = 13

289 assert _score(code) == 13

290

291

292# --- FileComplexity ---

293

294

295 def test_file_total():

296 code = """

297 def f(x):

298 if x: # +1

299 pass

300

301 def g(a, b):

302 if a: # +1

303 if b: # +1 +1 = 2

304 pass

305"""

306 result = analyze_source(code)

307 assert result.total == 4

308 assert len(result.functions) == 2

309

310

311 def test_function_lines():

312 code = """

313 def first():

314 pass

315

316 def second():

317 pass

318"""

319 result = analyze_source(code)

320 assert result.functions[0].line == 2

321 assert result.functions[0].name == "first"

322 assert result.functions[1].line == 5

323 assert result.functions[1].name == "second"

324

325

326# --- Edge cases ---

327

328

329 def test_empty_function():

330 assert _score("def f(): pass") == 0

331

332

333 def test_not_operator_no_increment():

334 """'not' is a unary operator, not a boolean sequence."""

335 code = """

336 def f(x):

337 if not x: # +1 (if only)

338 pass

339"""

340 assert _score(code) == 1