Skip to content

Commit 812b6bd

Browse files
authored
Merge pull request #10053 from erik-krogh/msgConsis-ql-query
QL: add ql/consistent-alert-message
2 parents 99c049c + d96dca4 commit 812b6bd

File tree

4 files changed

+102
-1
lines changed

4 files changed

+102
-1
lines changed

ql/ql/src/codeql_ql/ast/Ast.qll

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,29 @@ class QLDoc extends TQLDoc, Comment {
176176
override AstNode getParent() { result.getQLDoc() = this }
177177
}
178178

179+
/** The QLDoc for a query (i.e. the top comment in a .ql file). */
180+
class QueryDoc extends QLDoc {
181+
QueryDoc() {
182+
this.getLocation().getFile().getExtension() = "ql" and
183+
this = any(TopLevel t).getQLDoc()
184+
}
185+
186+
override string getAPrimaryQlClass() { result = "QueryDoc" }
187+
188+
/** Gets the @kind for the query */
189+
string getQueryKind() { result = this.getContents().regexpCapture("(?s).*@kind (\\w+)\\s.*", 1) }
190+
191+
/** Gets the id part (without language) of the @id */
192+
string getQueryId() {
193+
result = this.getContents().regexpCapture("(?s).*@id (\\w+)/([\\w\\-]+)\\s.*", 2)
194+
}
195+
196+
/** Gets the language of the @id */
197+
string getQueryLanguage() {
198+
result = this.getContents().regexpCapture("(?s).*@id (\\w+)/([\\w\\-]+)\\s.*", 1)
199+
}
200+
}
201+
179202
class BlockComment extends TBlockComment, Comment {
180203
QL::BlockComment comment;
181204

@@ -237,6 +260,8 @@ class Select extends TSelect, AstNode {
237260
}
238261

239262
override string getAPrimaryQlClass() { result = "Select" }
263+
264+
QueryDoc getQueryDoc() { result.getLocation().getFile() = this.getLocation().getFile() }
240265
}
241266

242267
class PredicateOrBuiltin extends TPredOrBuiltin, AstNode {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @name Consistent alert message
3+
* @description The alert message should be consistent across languages.
4+
* @kind problem
5+
* @problem.severity warning
6+
* @id ql/consistent-alert-message
7+
* @tags correctness
8+
* @precision very-high
9+
*/
10+
11+
import ql
12+
13+
/**
14+
* Gets a string representation of the entire message in `sel`.
15+
* Ignores everything that is not a string constant.
16+
*/
17+
string getMessage(Select sel) {
18+
result =
19+
strictconcat(String e, Location l |
20+
// is child of an expression in the select (in an odd-indexed position, that's where the message is)
21+
e.getParent*() = sel.getExpr(any(int i | i % 2 = 1)) and l = e.getFullLocation()
22+
|
23+
e.getValue(), " | " order by l.getStartLine(), l.getStartColumn()
24+
).trim()
25+
}
26+
27+
/**
28+
* Gets a language agnostic fingerprint for a Select.
29+
* The fingerPrint includes e.g. the query-id, the @kind of the query, and the number of expressions in the select.
30+
*
31+
* This fingerprint avoids false positives where two queries with the same ID behave differently (which is OK).
32+
*/
33+
string getSelectFingerPrint(Select sel) {
34+
exists(QueryDoc doc | doc = sel.getQueryDoc() |
35+
result =
36+
doc.getQueryId() // query ID (without lang)
37+
+ "-" + doc.getQueryKind() // @kind
38+
+ "-" +
39+
strictcount(String e | e.getParent*() = sel.getExpr(any(int i | i % 2 = 1))) // the number of string constants in the select
40+
+ "-" + count(sel.getExpr(_)) // and the total number of expressions in the select
41+
)
42+
}
43+
44+
/**
45+
* Gets information about the select.
46+
* The query-id (without language), the language, the message from the select, and a language agnostic fingerprint.
47+
*/
48+
Select parseSelect(string id, string lang, string msg, string fingerPrint) {
49+
exists(QueryDoc doc |
50+
doc = result.getQueryDoc() and
51+
id = doc.getQueryId() and
52+
lang = doc.getQueryLanguage() and
53+
fingerPrint = getSelectFingerPrint(result) and
54+
msg = getMessage(result).toLowerCase() // case normalize, because some languages upper-case methods.
55+
) and
56+
// excluding experimental
57+
not result.getLocation().getFile().getRelativePath().matches("%/experimental/%") and
58+
not lang = "ql" // excluding QL-for-QL
59+
}
60+
61+
from Select sel, string id, string lang, string msg, string fingerPrint, string otherLangs
62+
where
63+
// for a select with a fingerprint
64+
sel = parseSelect(id, lang, msg, fingerPrint) and
65+
// there exists other languages with the same fingerprint, but other message
66+
otherLangs =
67+
strictconcat(string bad |
68+
bad != lang and
69+
exists(parseSelect(id, bad, any(string otherMsg | otherMsg != msg), fingerPrint))
70+
|
71+
bad, ", "
72+
)
73+
select sel,
74+
"The " + lang + "/" + id + " query does not have the same alert message as " + otherLangs + "."

ql/ql/src/queries/style/docs/ClassDocs.ql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ predicate badStyle(string s) {
2222
from Class c
2323
where
2424
badStyle(c.getQLDoc().getContents()) and
25-
not c.isPrivate()
25+
not c.isPrivate() and
26+
not c.hasAnnotation("deprecated")
2627
select c.getQLDoc(), "The QLDoc for a class should start with 'A', 'An', or 'The'."

ql/ql/src/queries/style/docs/PredicateDocs.ql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ string docLines(Predicate pred) {
1818
from Predicate pred, string message
1919
where
2020
not pred.isPrivate() and
21+
not pred.hasAnnotation("deprecated") and
2122
// only considering qldocs that look like a class-doc, to avoid reporting way too much.
2223
docLines(pred).matches(["A", "An", "The"] + " %") and // looks like a class doc.
2324
not pred instanceof NewTypeBranch and // <- these are actually kinda class-like.

0 commit comments

Comments
 (0)