Files
codeql/python/ql/lib/semmle/python/Metrics.qll
Taus 156d2c09a0 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).
2026-03-09 17:22:01 +00:00

210 lines
6.9 KiB
Plaintext

import python
private import semmle.python.SelfAttribute
private import semmle.python.dataflow.new.internal.DataFlowDispatch
/** The metrics for a function */
class FunctionMetrics extends Function {
/**
* Gets the total number of lines (including blank lines)
* from the definition to the end of the function
*/
int getNumberOfLines() { py_alllines(this, result) }
/** Gets the number of lines of code in the function */
int getNumberOfLinesOfCode() { py_codelines(this, result) }
/** Gets the number of lines of comments in the function */
int getNumberOfLinesOfComments() { py_commentlines(this, result) }
/** Gets the number of lines of docstring in the function */
int getNumberOfLinesOfDocStrings() { py_docstringlines(this, result) }
int getNumberOfParametersWithoutDefault() {
result =
this.getPositionalParameterCount() -
count(this.getDefinition().(FunctionExpr).getArgs().getADefault())
}
int getStatementNestingDepth() { result = max(Stmt s | s.getScope() = this | getNestingDepth(s)) }
int getNumberOfCalls() { result = count(Call c | c.getScope() = this) }
/**
* Gets the cyclomatic complexity of the function:
* The number of linearly independent paths through the source code.
* Computed as E - N + 2P,
* where
* E = the number of edges of the graph.
* N = the number of nodes of the graph.
* P = the number of connected components, which for a single function is 1.
*/
int getCyclomaticComplexity() {
exists(int n, int e |
n = count(BasicBlock b | b.getScope() = this and Reachability::likelyReachable(b)) and
e =
count(BasicBlock b1, BasicBlock b2 |
b1.getScope() = this and
Reachability::likelyReachable(b1) and
b2.getScope() = this and
Reachability::likelyReachable(b2) and
b2 = b1.getASuccessor() and
not Reachability::unlikelySuccessor(b1.getLastNode(), b2.firstNode())
)
|
result = e - n + 2
)
}
}
/** The metrics for a class */
class ClassMetrics extends Class {
/**
* Gets the total number of lines (including blank lines)
* from the definition to the end of the class
*/
int getNumberOfLines() { py_alllines(this, result) }
/** Gets the number of lines of code in the class */
int getNumberOfLinesOfCode() { py_codelines(this, result) }
/** Gets the number of lines of comments in the class */
int getNumberOfLinesOfComments() { py_commentlines(this, result) }
/** Gets the number of lines of docstrings in the class */
int getNumberOfLinesOfDocStrings() { py_docstringlines(this, result) }
/* -------- CHIDAMBER AND KEMERER LACK OF COHESION IN METHODS ------------ */
/*
* The aim of this metric is to try and determine whether a class
* represents one abstraction (good) or multiple abstractions (bad).
* If a class represents multiple abstractions, it should be split
* up into multiple classes.
*
* In the Chidamber and Kemerer method, this is measured as follows:
* n1 = number of pairs of distinct methods in a class that do *not*
* have at least one commonly accessed field
* n2 = number of pairs of distinct methods in a class that do
* have at least one commonly accessed field
* lcom = ((n1 - n2)/2 max 0)
*
* We divide by 2 because each pair (m1,m2) is counted twice in n1 and n2.
*/
/** should function f be excluded from the cohesion computation? */
predicate ignoreLackOfCohesion(Function f) { f.isInitMethod() or f.isSpecialMethod() }
private predicate methodPair(Function m1, Function m2) {
m1.getScope() = this and
m2.getScope() = this and
not this.ignoreLackOfCohesion(m1) and
not this.ignoreLackOfCohesion(m2) and
m1 != m2
}
private predicate one_accesses_other(Function m1, Function m2) {
this.methodPair(m1, m2) and
(
exists(SelfAttributeRead sa |
sa.getName() = m1.getName() and
sa.getScope() = m2
)
or
exists(SelfAttributeRead sa |
sa.getName() = m2.getName() and
sa.getScope() = m1
)
)
}
/** do m1 and m2 access a common field or one calls the other? */
private predicate shareField(Function m1, Function m2) {
this.methodPair(m1, m2) and
exists(string name |
exists(SelfAttributeRead sa |
sa.getName() = name and
sa.getScope() = m1
) and
exists(SelfAttributeRead sa |
sa.getName() = name and
sa.getScope() = m2
)
)
}
private int similarMethodPairs() {
result =
count(Function m1, Function m2 |
this.methodPair(m1, m2) and
(this.shareField(m1, m2) or this.one_accesses_other(m1, m2))
) / 2
}
private int methodPairs() {
result = count(Function m1, Function m2 | this.methodPair(m1, m2)) / 2
}
/** return Chidamber and Kemerer Lack of Cohesion */
int getLackOfCohesionCK() {
exists(int n |
n = this.methodPairs() - 2 * this.similarMethodPairs() and
result = n.maximum(0)
)
}
private predicate similarMethodPairDag(Function m1, Function m2, int line) {
(this.shareField(m1, m2) or this.one_accesses_other(m1, m2)) and
line = m1.getLocation().getStartLine() and
line < m2.getLocation().getStartLine()
}
private predicate subgraph(Function m, int line) {
this.similarMethodPairDag(m, _, line) and not this.similarMethodPairDag(_, m, _)
or
exists(Function other | this.subgraph(other, line) |
this.similarMethodPairDag(other, m, _) or
this.similarMethodPairDag(m, other, _)
)
}
predicate unionSubgraph(Function m, int line) { line = min(int l | this.subgraph(m, l)) }
/** return Hitz and Montazeri Lack of Cohesion */
int getLackOfCohesionHM() { result = count(int line | this.unionSubgraph(_, line)) }
}
class ModuleMetrics extends Module {
/** Gets the total number of lines (including blank lines) in the module */
int getNumberOfLines() { py_alllines(this, result) }
/** Gets the number of lines of code in the module */
int getNumberOfLinesOfCode() { py_codelines(this, result) }
/** Gets the number of lines of comments in the module */
int getNumberOfLinesOfComments() { py_commentlines(this, result) }
/** Gets the number of lines of docstrings in the module */
int getNumberOfLinesOfDocStrings() { py_docstringlines(this, result) }
}
predicate non_coupling_method(Function f) {
f.isSpecialMethod() or
f.isInitMethod() or
f.getName() = "close" or
f.getName() = "write" or
f.getName() = "read" or
f.getName() = "get" or
f.getName() = "set"
}
private int getNestingDepth(Stmt s) {
not exists(Stmt outer | outer.getASubStatement() = s) and result = 1
or
exists(Stmt outer | outer.getASubStatement() = s |
if s.(If).isElif() or s instanceof ExceptStmt
then
/* If statement is an `elif` or `except` then it is not indented relative to its parent */
result = getNestingDepth(outer)
else result = getNestingDepth(outer) + 1
)
}