Skip to content

Commit 463f79b

Browse files
Merge pull request #20263 from joefarebrother/python-qual-exceptions
Python: Modernize the Unreachable Except Block query
2 parents be260be + ff4c11f commit 463f79b

File tree

11 files changed

+894
-22
lines changed

11 files changed

+894
-22
lines changed

python/ql/lib/semmle/python/frameworks/builtins.model.yml

Lines changed: 738 additions & 0 deletions
Large diffs are not rendered by default.

python/ql/src/Exceptions/IncorrectExceptOrder.qhelp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,20 @@ is a super class of <code>Error</code>.
2525

2626
<p>Reorganize the <code>except</code> blocks so that the more specific <code>except</code>
2727
is defined first. Alternatively, if the more specific <code>except</code> block is
28-
no longer required then it should be deleted.</p>
28+
no longer required, then it should be deleted.</p>
2929

3030
</recommendation>
3131
<example>
32-
<p>In this example the <code>except Exception:</code> will handle <code>AttributeError</code> preventing the
32+
<p>In the following example, the <code>except Exception:</code> will handle <code>AttributeError</code> preventing the
3333
subsequent handler from ever executing.</p>
3434
<sample src="IncorrectExceptOrder.py" />
3535

3636

3737
</example>
3838
<references>
3939

40-
<li>Python Language Reference: <a href="http://docs.python.org/2.7/reference/compound_stmts.html#try">The try statement</a>,
41-
<a href="http://docs.python.org/2.7/reference/executionmodel.html#exceptions">Exceptions</a>.</li>
40+
<li>Python Language Reference: <a href="http://docs.python.org/3/reference/compound_stmts.html#try">The try statement</a>,
41+
<a href="http://docs.python.org/3/reference/executionmodel.html#exceptions">Exceptions</a>.</li>
4242

4343

4444
</references>
Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* @name Unreachable 'except' block
2+
* @name Unreachable `except` block
33
* @description Handling general exceptions before specific exceptions means that the specific
44
* handlers are never executed.
55
* @kind problem
@@ -14,22 +14,95 @@
1414
*/
1515

1616
import python
17+
import semmle.python.dataflow.new.internal.DataFlowDispatch
18+
import semmle.python.ApiGraphs
19+
import semmle.python.frameworks.data.internal.ApiGraphModels
1720

18-
predicate incorrect_except_order(ExceptStmt ex1, ClassValue cls1, ExceptStmt ex2, ClassValue cls2) {
21+
predicate builtinException(string name) {
22+
typeModel("builtins.BaseException~Subclass", "builtins." + name, "")
23+
}
24+
25+
predicate builtinExceptionSubclass(string base, string sub) {
26+
typeModel("builtins." + base + "~Subclass", "builtins." + sub, "")
27+
}
28+
29+
newtype TExceptType =
30+
TClass(Class c) or
31+
TBuiltin(string name) { builtinException(name) }
32+
33+
class ExceptType extends TExceptType {
34+
Class asClass() { this = TClass(result) }
35+
36+
string asBuiltinName() { this = TBuiltin(result) }
37+
38+
predicate isBuiltin() { this = TBuiltin(_) }
39+
40+
string getName() {
41+
result = this.asClass().getName()
42+
or
43+
result = this.asBuiltinName()
44+
}
45+
46+
string toString() { result = this.getName() }
47+
48+
DataFlow::Node getAUse() {
49+
result = classTracker(this.asClass())
50+
or
51+
API::builtin(this.asBuiltinName()).asSource().flowsTo(result)
52+
}
53+
54+
ExceptType getADirectSuperclass() {
55+
result.asClass() = getADirectSuperclass(this.asClass())
56+
or
57+
result.isBuiltin() and
58+
result.getAUse().asExpr() = this.asClass().getABase()
59+
or
60+
builtinExceptionSubclass(result.asBuiltinName(), this.asBuiltinName()) and
61+
this != result
62+
}
63+
64+
/**
65+
* Holds if this element is at the specified location.
66+
* The location spans column `startColumn` of line `startLine` to
67+
* column `endColumn` of line `endLine` in file `filepath`.
68+
* For more information, see
69+
* [Providing locations in CodeQL queries](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/).
70+
*/
71+
predicate hasLocationInfo(
72+
string filePath, int startLine, int startColumn, int endLine, int endColumn
73+
) {
74+
this.asClass()
75+
.getLocation()
76+
.hasLocationInfo(filePath, startLine, startColumn, endLine, endColumn)
77+
or
78+
this.isBuiltin() and
79+
filePath = "" and
80+
startLine = 0 and
81+
startColumn = 0 and
82+
endLine = 0 and
83+
endColumn = 0
84+
}
85+
}
86+
87+
predicate incorrectExceptOrder(ExceptStmt ex1, ExceptType cls1, ExceptStmt ex2, ExceptType cls2) {
1988
exists(int i, int j, Try t |
2089
ex1 = t.getHandler(i) and
2190
ex2 = t.getHandler(j) and
2291
i < j and
23-
cls1 = except_class(ex1) and
24-
cls2 = except_class(ex2) and
25-
cls1 = cls2.getASuperType()
92+
cls1 = exceptClass(ex1) and
93+
cls2 = exceptClass(ex2) and
94+
cls1 = cls2.getADirectSuperclass*()
2695
)
2796
}
2897

29-
ClassValue except_class(ExceptStmt ex) { ex.getType().pointsTo(result) }
98+
ExceptType exceptClass(ExceptStmt ex) { ex.getType() = result.getAUse().asExpr() }
3099

31-
from ExceptStmt ex1, ClassValue cls1, ExceptStmt ex2, ClassValue cls2
32-
where incorrect_except_order(ex1, cls1, ex2, cls2)
33-
select ex2,
34-
"Except block for $@ is unreachable; the more general $@ for $@ will always be executed in preference.",
35-
cls2, cls2.getName(), ex1, "except block", cls1, cls1.getName()
100+
from ExceptStmt ex1, ExceptType cls1, ExceptStmt ex2, ExceptType cls2, string msg
101+
where
102+
incorrectExceptOrder(ex1, cls1, ex2, cls2) and
103+
if cls1 = cls2
104+
then msg = "This except block handling $@ is unreachable; as $@ also handles $@."
105+
else
106+
msg =
107+
"This except block handling $@ is unreachable; as $@ for the more general $@ always subsumes it."
108+
select ex2, msg, cls2, cls2.getName(), ex1, "this except block", cls1, cls1.getName()
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from shared_subclass_functions import wrap_in_template
2+
import sys
3+
import yaml
4+
from pathlib import Path
5+
6+
py_version = sys.version.split()[0]
7+
VERSION = f"process-builtin-exceptions 0.0.1; Python {py_version}"
8+
9+
builtins_model_path = Path(__file__).parent.parent.parent.parent / "lib/semmle/python/frameworks/builtins.model.yml"
10+
11+
def write_data(data, path: Path):
12+
f = path.open("w+")
13+
f.write(f"# {VERSION}\n")
14+
yaml.dump(data, indent=2, stream=f, Dumper=yaml.CDumper)
15+
16+
builtin_names = dir(__builtins__)
17+
builtin_dict = {x: getattr(__builtins__,x) for x in builtin_names}
18+
19+
20+
builtin_exceptions = {v for v in builtin_dict.values() if type(v) is type and issubclass(v, BaseException)}
21+
22+
data = []
23+
for sub in builtin_exceptions:
24+
for base in sub.__mro__:
25+
if issubclass(base, BaseException):
26+
basename = base.__name__
27+
subname = sub.__name__
28+
row = [f"builtins.{basename}~Subclass", f"builtins.{subname}", ""]
29+
data.append(row)
30+
31+
write_data(wrap_in_template(data), builtins_model_path)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
| exceptions_test.py:7:5:7:11 | ExceptStmt | Except block directly handles BaseException. |
2-
| exceptions_test.py:71:5:71:25 | ExceptStmt | Except block directly handles BaseException. |
2+
| exceptions_test.py:97:5:97:25 | ExceptStmt | Except block directly handles BaseException. |
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
| exceptions_test.py:7:5:7:11 | ExceptStmt | 'except' clause does nothing but pass and there is no explanatory comment. |
22
| exceptions_test.py:13:5:13:21 | ExceptStmt | 'except' clause does nothing but pass and there is no explanatory comment. |
3+
| exceptions_test.py:72:1:72:18 | ExceptStmt | 'except' clause does nothing but pass and there is no explanatory comment. |
4+
| exceptions_test.py:85:1:85:17 | ExceptStmt | 'except' clause does nothing but pass and there is no explanatory comment. |
5+
| exceptions_test.py:89:1:89:17 | ExceptStmt | 'except' clause does nothing but pass and there is no explanatory comment. |
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
| exceptions_test.py:51:5:51:25 | ExceptStmt | Non-exception $@ in exception handler which will never match raised exception. | exceptions_test.py:33:1:33:28 | ControlFlowNode for ClassExpr | class 'NotException1' |
22
| exceptions_test.py:54:5:54:25 | ExceptStmt | Non-exception $@ in exception handler which will never match raised exception. | exceptions_test.py:36:1:36:28 | ControlFlowNode for ClassExpr | class 'NotException2' |
3-
| exceptions_test.py:112:5:112:22 | ExceptStmt | Non-exception $@ in exception handler which will never match raised exception. | exceptions_test.py:107:12:107:14 | ControlFlowNode for FloatLiteral | instance of 'float' |
3+
| exceptions_test.py:138:5:138:22 | ExceptStmt | Non-exception $@ in exception handler which will never match raised exception. | exceptions_test.py:133:12:133:14 | ControlFlowNode for FloatLiteral | instance of 'float' |
44
| pypy_test.py:14:5:14:14 | ExceptStmt | Non-exception $@ in exception handler which will never match raised exception. | pypy_test.py:14:12:14:13 | ControlFlowNode for IntegerLiteral | instance of 'int' |
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
| exceptions_test.py:64:1:64:22 | ExceptStmt | Except block for $@ is unreachable; the more general $@ for $@ will always be executed in preference. | file://:0:0:0:0 | builtin-class AttributeError | AttributeError | exceptions_test.py:62:1:62:17 | ExceptStmt | except block | file://:0:0:0:0 | builtin-class Exception | Exception |
1+
| exceptions_test.py:64:1:64:22 | ExceptStmt | This except block handling $@ is unreachable; as $@ for the more general $@ always subsumes it. | file://:0:0:0:0 | AttributeError | AttributeError | exceptions_test.py:62:1:62:17 | ExceptStmt | this except block | file://:0:0:0:0 | Exception | Exception |
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
Exceptions/IncorrectExceptOrder.ql
1+
query: Exceptions/IncorrectExceptOrder.ql
2+
postprocess: utils/test/InlineExpectationsTestQuery.ql
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
| exceptions_test.py:170:11:170:24 | NotImplemented | NotImplemented is not an Exception. Did you mean NotImplementedError? |
2-
| exceptions_test.py:173:11:173:26 | NotImplemented() | NotImplemented is not an Exception. Did you mean NotImplementedError? |
1+
| exceptions_test.py:196:11:196:24 | NotImplemented | NotImplemented is not an Exception. Did you mean NotImplementedError? |
2+
| exceptions_test.py:199:11:199:26 | NotImplemented() | NotImplemented is not an Exception. Did you mean NotImplementedError? |

0 commit comments

Comments
 (0)