Skip to content

Commit 156d2c0

Browse files
committed
Python: Port getCyclomaticComplexity function
Note that this does not give the exact same results as the old function, however it's not clear to me that the old results were actually correct (it _looks_ like `read()` might be doing an IO operation, but in fact `read` is not defined, so at best this will raise a NameError, not an IOError).
1 parent 7d8b4ac commit 156d2c0

File tree

5 files changed

+47
-35
lines changed

5 files changed

+47
-35
lines changed

python/ql/lib/LegacyPointsTo.qll

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -433,38 +433,6 @@ private predicate exits_early(BasicBlock b) {
433433

434434
/** The metrics for a function that require points-to analysis */
435435
class FunctionMetricsWithPointsTo extends FunctionMetrics {
436-
/**
437-
* Gets the cyclomatic complexity of the function:
438-
* The number of linearly independent paths through the source code.
439-
* Computed as E - N + 2P,
440-
* where
441-
* E = the number of edges of the graph.
442-
* N = the number of nodes of the graph.
443-
* P = the number of connected components, which for a single function is 1.
444-
*/
445-
int getCyclomaticComplexity() {
446-
exists(int e, int n |
447-
n = count(BasicBlockWithPointsTo b | b = this.getABasicBlock() and b.likelyReachable()) and
448-
e =
449-
count(BasicBlockWithPointsTo b1, BasicBlockWithPointsTo b2 |
450-
b1 = this.getABasicBlock() and
451-
b1.likelyReachable() and
452-
b2 = this.getABasicBlock() and
453-
b2.likelyReachable() and
454-
b2 = b1.getASuccessor() and
455-
not b1.unlikelySuccessor(b2)
456-
)
457-
|
458-
result = e - n + 2
459-
)
460-
}
461-
462-
private BasicBlock getABasicBlock() {
463-
result = this.getEntryNode().getBasicBlock()
464-
or
465-
exists(BasicBlock mid | mid = this.getABasicBlock() and result = mid.getASuccessor())
466-
}
467-
468436
/**
469437
* Dependency of Callables
470438
* One callable "this" depends on another callable "result"

python/ql/lib/semmle/python/Metrics.qll

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import python
22
private import semmle.python.SelfAttribute
3+
private import semmle.python.dataflow.new.internal.DataFlowDispatch
34

45
/** The metrics for a function */
56
class FunctionMetrics extends Function {
@@ -27,6 +28,32 @@ class FunctionMetrics extends Function {
2728
int getStatementNestingDepth() { result = max(Stmt s | s.getScope() = this | getNestingDepth(s)) }
2829

2930
int getNumberOfCalls() { result = count(Call c | c.getScope() = this) }
31+
32+
/**
33+
* Gets the cyclomatic complexity of the function:
34+
* The number of linearly independent paths through the source code.
35+
* Computed as E - N + 2P,
36+
* where
37+
* E = the number of edges of the graph.
38+
* N = the number of nodes of the graph.
39+
* P = the number of connected components, which for a single function is 1.
40+
*/
41+
int getCyclomaticComplexity() {
42+
exists(int n, int e |
43+
n = count(BasicBlock b | b.getScope() = this and Reachability::likelyReachable(b)) and
44+
e =
45+
count(BasicBlock b1, BasicBlock b2 |
46+
b1.getScope() = this and
47+
Reachability::likelyReachable(b1) and
48+
b2.getScope() = this and
49+
Reachability::likelyReachable(b2) and
50+
b2 = b1.getASuccessor() and
51+
not Reachability::unlikelySuccessor(b1.getLastNode(), b2.firstNode())
52+
)
53+
|
54+
result = e - n + 2
55+
)
56+
}
3057
}
3158

3259
/** The metrics for a class */

python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2345,6 +2345,19 @@ module Reachability {
23452345
)
23462346
}
23472347

2348+
/**
2349+
* Holds if `node` is unlikely to raise an exception. This includes entry nodes
2350+
* and simple name lookups.
2351+
*/
2352+
private predicate unlikelyToRaise(ControlFlowNode node) {
2353+
exists(node.getAnExceptionalSuccessor()) and
2354+
(
2355+
node.getNode() instanceof Name
2356+
or
2357+
exists(Scope s | s.getEntryNode() = node)
2358+
)
2359+
}
2360+
23482361
/**
23492362
* Holds if it is highly unlikely for control to flow from `node` to `succ`.
23502363
*/
@@ -2357,6 +2370,10 @@ module Reachability {
23572370
succ = node.getASuccessor() and
23582371
not succ = node.getAnExceptionalSuccessor() and
23592372
not succ.getNode() instanceof Yield
2373+
or
2374+
// Exception edge from a node that is unlikely to raise
2375+
unlikelyToRaise(node) and
2376+
succ = node.getAnExceptionalSuccessor()
23602377
}
23612378

23622379
private predicate startBbLikelyReachable(BasicBlock b) {

python/ql/src/Metrics/CyclomaticComplexity.ql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
*/
1414

1515
import python
16-
private import LegacyPointsTo
16+
import semmle.python.Metrics
1717

18-
from FunctionMetricsWithPointsTo func, int complexity
18+
from FunctionMetrics func, int complexity
1919
where complexity = func.getCyclomaticComplexity()
2020
select func, complexity order by complexity desc
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
| code.py:35:1:35:21 | Function exceptions | 5 |
12
| code.py:23:1:23:17 | Function nested | 4 |
23
| code.py:12:1:12:21 | Function two_branch | 3 |
34
| code.py:6:1:6:18 | Function one_branch | 2 |
4-
| code.py:35:1:35:21 | Function exceptions | 2 |
55
| code.py:1:1:1:16 | Function f_linear | 1 |
66
| code.py:45:1:45:39 | Function must_be_positive | 1 |

0 commit comments

Comments
 (0)