Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
20fea39
Python: Remove points-to from `Metrics.qll`
tausbn Feb 18, 2026
e8de843
Python: Update all metrics-dependant queries
tausbn Feb 18, 2026
07099f1
Python: Add change note
tausbn Feb 18, 2026
8a67917
Python: Use API graphs instead of points-to for simple built-ins
tausbn Feb 18, 2026
0174237
Python: Port `py/print-during-import`
tausbn Feb 19, 2026
40a8e22
Python: Introduce `DuckTyping` module
tausbn Feb 20, 2026
3b686c0
Python: Port ContainsNonContainer.ql
tausbn Feb 20, 2026
9717e3f
Python: Port NonIteratorInForLoop.ql
tausbn Feb 20, 2026
69f42c4
Python: Port ShouldUseWithStatement.ql
tausbn Feb 20, 2026
576b560
Python: Port UnusedExceptionObject.ql
tausbn Feb 20, 2026
2fce1b3
Python: Add `DuckTyping::isNewStyle`
tausbn Feb 20, 2026
48e51cd
Python: Add declares/getAttribute API
tausbn Feb 20, 2026
890ce56
Python: Port SlotsInOldStyleClass.ql
tausbn Feb 20, 2026
9847c83
Python: Port SuperInOldStyleClass.ql
tausbn Feb 20, 2026
e09a4ba
Python: Port PropertyInOldStyleClass.ql
tausbn Feb 20, 2026
6826c4e
Python: Port InconsistentMRO.ql
tausbn Feb 20, 2026
7b34f11
Python: Port HashedButNoHash.ql
tausbn Feb 20, 2026
f64811a
Python: Port UselessClass.ql
tausbn Feb 20, 2026
cd4dbb4
Python: Port ShouldBeContextManager.ql
tausbn Feb 20, 2026
60f0bd2
Python: Port WrongNameForArgumentInClassInstantiation.ql
tausbn Feb 23, 2026
250538b
Python: Port WrongNumberArgumentsInClassInstantiation.ql
tausbn Feb 23, 2026
093207e
Python: Remove missing results
tausbn Feb 23, 2026
61ced7d
Python: Extend DuckTyping module
tausbn Feb 23, 2026
505ad22
Python: Port DeprecatedSliceMethod.ql
tausbn Feb 23, 2026
11758cb
Python: Port DocStrings.ql
tausbn Feb 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions python/ql/lib/LegacyPointsTo.qll
Original file line number Diff line number Diff line change
Expand Up @@ -430,3 +430,179 @@ private predicate exits_early(BasicBlock b) {
f.getACall().getBasicBlock() = b
)
}

/** The metrics for a function that require points-to analysis */
class FunctionMetricsWithPointsTo extends FunctionMetrics {
/**
* 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 e, int n |
n = count(BasicBlockWithPointsTo b | b = this.getABasicBlock() and b.likelyReachable()) and
e =
count(BasicBlockWithPointsTo b1, BasicBlockWithPointsTo b2 |
b1 = this.getABasicBlock() and
b1.likelyReachable() and
b2 = this.getABasicBlock() and
b2.likelyReachable() and
b2 = b1.getASuccessor() and
not b1.unlikelySuccessor(b2)
)
|
result = e - n + 2
)
}

private BasicBlock getABasicBlock() {
result = this.getEntryNode().getBasicBlock()
or
exists(BasicBlock mid | mid = this.getABasicBlock() and result = mid.getASuccessor())
}

/**
* Dependency of Callables
* One callable "this" depends on another callable "result"
* if "this" makes some call to a method that may end up being "result".
*/
FunctionMetricsWithPointsTo getADependency() {
result != this and
not non_coupling_method(result) and
exists(Call call | call.getScope() = this |
exists(FunctionObject callee | callee.getFunction() = result |
call.getAFlowNode().getFunction().(ControlFlowNodeWithPointsTo).refersTo(callee)
)
or
exists(Attribute a | call.getFunc() = a |
unique_root_method(result, a.getName())
or
exists(Name n | a.getObject() = n and n.getId() = "self" |
result.getScope() = this.getScope() and
result.getName() = a.getName()
)
)
)
}

/**
* Afferent Coupling
* the number of callables that depend on this method.
* This is sometimes called the "fan-in" of a method.
*/
int getAfferentCoupling() {
result = count(FunctionMetricsWithPointsTo m | m.getADependency() = this)
}

/**
* Efferent Coupling
* the number of methods that this method depends on
* This is sometimes called the "fan-out" of a method.
*/
int getEfferentCoupling() {
result = count(FunctionMetricsWithPointsTo m | this.getADependency() = m)
}

override string getAQlClass() { result = "FunctionMetrics" }
}

/** The metrics for a class that require points-to analysis */
class ClassMetricsWithPointsTo extends ClassMetrics {
private predicate dependsOn(Class other) {
other != this and
(
exists(FunctionMetricsWithPointsTo f1, FunctionMetricsWithPointsTo f2 |
f1.getADependency() = f2
|
f1.getScope() = this and f2.getScope() = other
)
or
exists(Function f, Call c, ClassObject cls | c.getScope() = f and f.getScope() = this |
c.getFunc().(ExprWithPointsTo).refersTo(cls) and
cls.getPyClass() = other
)
)
}

/**
* Gets the afferent coupling of a class -- the number of classes that
* directly depend on it.
*/
int getAfferentCoupling() { result = count(ClassMetricsWithPointsTo t | t.dependsOn(this)) }

/**
* Gets the efferent coupling of a class -- the number of classes that
* it directly depends on.
*/
int getEfferentCoupling() { result = count(ClassMetricsWithPointsTo t | this.dependsOn(t)) }

/** Gets the depth of inheritance of the class. */
int getInheritanceDepth() {
exists(ClassObject cls | cls.getPyClass() = this | result = max(classInheritanceDepth(cls)))
}

override string getAQlClass() { result = "ClassMetrics" }
}

private int classInheritanceDepth(ClassObject cls) {
/* Prevent run-away recursion in case of circular inheritance */
not cls.getASuperType() = cls and
(
exists(ClassObject sup | cls.getABaseType() = sup | result = classInheritanceDepth(sup) + 1)
or
not exists(cls.getABaseType()) and
(
major_version() = 2 and result = 0
or
major_version() > 2 and result = 1
)
)
}

/** The metrics for a module that require points-to analysis */
class ModuleMetricsWithPointsTo extends ModuleMetrics {
/**
* Gets the afferent coupling of a module -- the number of modules that
* directly depend on it.
*/
int getAfferentCoupling() { result = count(ModuleMetricsWithPointsTo t | t.dependsOn(this)) }

/**
* Gets the efferent coupling of a module -- the number of modules that
* it directly depends on.
*/
int getEfferentCoupling() { result = count(ModuleMetricsWithPointsTo t | this.dependsOn(t)) }

private predicate dependsOn(Module other) {
other != this and
(
exists(FunctionMetricsWithPointsTo f1, FunctionMetricsWithPointsTo f2 |
f1.getADependency() = f2
|
f1.getEnclosingModule() = this and f2.getEnclosingModule() = other
)
or
exists(Function f, Call c, ClassObject cls | c.getScope() = f and f.getScope() = this |
c.getFunc().(ExprWithPointsTo).refersTo(cls) and
cls.getPyClass().getEnclosingModule() = other
)
)
}

override string getAQlClass() { result = "ModuleMetrics" }
}

/** Helpers for coupling */
predicate unique_root_method(Function func, string name) {
name = func.getName() and
not exists(FunctionObject f, FunctionObject other |
f.getFunction() = func and
other.getName() = name
|
not other.overrides(f)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
category: minorAnalysis
---

- The `Metrics` library no longer contains code that depends on the points-to analysis. The removed functionality has instead been moved to the `LegacyPointsTo` module, to classes like `ModuleMetricsWithPointsTo` etc. If you depend on any of these classes, you must now remember to import `LegacyPointsTo`, and use the appropriate types in order to use the points-to-based functionality.
2 changes: 1 addition & 1 deletion python/ql/lib/python.qll
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import semmle.python.Patterns
import semmle.python.Keywords
import semmle.python.Comprehensions
import semmle.python.Flow
private import semmle.python.Metrics
import semmle.python.Metrics
import semmle.python.Constants
import semmle.python.Scope
import semmle.python.Comment
Expand Down
154 changes: 1 addition & 153 deletions python/ql/lib/semmle/python/Metrics.qll
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import python
private import LegacyPointsTo
private import semmle.python.SelfAttribute

/** The metrics for a function */
class FunctionMetrics extends Function {
Expand All @@ -18,76 +18,6 @@ class FunctionMetrics extends Function {
/** Gets the number of lines of docstring in the function */
int getNumberOfLinesOfDocStrings() { py_docstringlines(this, result) }

/**
* 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 e, int n |
n = count(BasicBlockWithPointsTo b | b = this.getABasicBlock() and b.likelyReachable()) and
e =
count(BasicBlockWithPointsTo b1, BasicBlockWithPointsTo b2 |
b1 = this.getABasicBlock() and
b1.likelyReachable() and
b2 = this.getABasicBlock() and
b2.likelyReachable() and
b2 = b1.getASuccessor() and
not b1.unlikelySuccessor(b2)
)
|
result = e - n + 2
)
}

private BasicBlock getABasicBlock() {
result = this.getEntryNode().getBasicBlock()
or
exists(BasicBlock mid | mid = this.getABasicBlock() and result = mid.getASuccessor())
}

/**
* Dependency of Callables
* One callable "this" depends on another callable "result"
* if "this" makes some call to a method that may end up being "result".
*/
FunctionMetrics getADependency() {
result != this and
not non_coupling_method(result) and
exists(Call call | call.getScope() = this |
exists(FunctionObject callee | callee.getFunction() = result |
call.getAFlowNode().getFunction().(ControlFlowNodeWithPointsTo).refersTo(callee)
)
or
exists(Attribute a | call.getFunc() = a |
unique_root_method(result, a.getName())
or
exists(Name n | a.getObject() = n and n.getId() = "self" |
result.getScope() = this.getScope() and
result.getName() = a.getName()
)
)
)
}

/**
* Afferent Coupling
* the number of callables that depend on this method.
* This is sometimes called the "fan-in" of a method.
*/
int getAfferentCoupling() { result = count(FunctionMetrics m | m.getADependency() = this) }

/**
* Efferent Coupling
* the number of methods that this method depends on
* This is sometimes called the "fan-out" of a method.
*/
int getEfferentCoupling() { result = count(FunctionMetrics m | this.getADependency() = m) }

int getNumberOfParametersWithoutDefault() {
result =
this.getPositionalParameterCount() -
Expand Down Expand Up @@ -116,36 +46,6 @@ class ClassMetrics extends Class {
/** Gets the number of lines of docstrings in the class */
int getNumberOfLinesOfDocStrings() { py_docstringlines(this, result) }

private predicate dependsOn(Class other) {
other != this and
(
exists(FunctionMetrics f1, FunctionMetrics f2 | f1.getADependency() = f2 |
f1.getScope() = this and f2.getScope() = other
)
or
exists(Function f, Call c, ClassObject cls | c.getScope() = f and f.getScope() = this |
c.getFunc().(ExprWithPointsTo).refersTo(cls) and
cls.getPyClass() = other
)
)
}

/**
* Gets the afferent coupling of a class -- the number of classes that
* directly depend on it.
*/
int getAfferentCoupling() { result = count(ClassMetrics t | t.dependsOn(this)) }

/**
* Gets the efferent coupling of a class -- the number of classes that
* it directly depends on.
*/
int getEfferentCoupling() { result = count(ClassMetrics t | this.dependsOn(t)) }

int getInheritanceDepth() {
exists(ClassObject cls | cls.getPyClass() = this | result = max(classInheritanceDepth(cls)))
}

/* -------- CHIDAMBER AND KEMERER LACK OF COHESION IN METHODS ------------ */
/*
* The aim of this metric is to try and determine whether a class
Expand Down Expand Up @@ -245,21 +145,6 @@ class ClassMetrics extends Class {
int getLackOfCohesionHM() { result = count(int line | this.unionSubgraph(_, line)) }
}

private int classInheritanceDepth(ClassObject cls) {
/* Prevent run-away recursion in case of circular inheritance */
not cls.getASuperType() = cls and
(
exists(ClassObject sup | cls.getABaseType() = sup | result = classInheritanceDepth(sup) + 1)
or
not exists(cls.getABaseType()) and
(
major_version() = 2 and result = 0
or
major_version() > 2 and result = 1
)
)
}

class ModuleMetrics extends Module {
/** Gets the total number of lines (including blank lines) in the module */
int getNumberOfLines() { py_alllines(this, result) }
Expand All @@ -272,43 +157,6 @@ class ModuleMetrics extends Module {

/** Gets the number of lines of docstrings in the module */
int getNumberOfLinesOfDocStrings() { py_docstringlines(this, result) }

/**
* Gets the afferent coupling of a class -- the number of classes that
* directly depend on it.
*/
int getAfferentCoupling() { result = count(ModuleMetrics t | t.dependsOn(this)) }

/**
* Gets the efferent coupling of a class -- the number of classes that
* it directly depends on.
*/
int getEfferentCoupling() { result = count(ModuleMetrics t | this.dependsOn(t)) }

private predicate dependsOn(Module other) {
other != this and
(
exists(FunctionMetrics f1, FunctionMetrics f2 | f1.getADependency() = f2 |
f1.getEnclosingModule() = this and f2.getEnclosingModule() = other
)
or
exists(Function f, Call c, ClassObject cls | c.getScope() = f and f.getScope() = this |
c.getFunc().(ExprWithPointsTo).refersTo(cls) and
cls.getPyClass().getEnclosingModule() = other
)
)
}
}

/** Helpers for coupling */
predicate unique_root_method(Function func, string name) {
name = func.getName() and
not exists(FunctionObject f, FunctionObject other |
f.getFunction() = func and
other.getName() = name
|
not other.overrides(f)
)
}

predicate non_coupling_method(Function f) {
Expand Down
Loading