Skip to content

Commit d816f7f

Browse files
committed
add ql/consistent-alert-message
1 parent 79bae0c commit d816f7f

File tree

1 file changed

+77
-0
lines changed

1 file changed

+77
-0
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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 uneven 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 avoid false positives where two queries with the same ID behave differently (which is OK).
32+
*/
33+
string getSelectFingerPrint(Select sel) {
34+
exists(File file, QLDoc doc |
35+
sel.getLocation().getFile() = file and
36+
any(TopLevel top | top.getLocation().getFile() = file).getQLDoc() = doc
37+
|
38+
result =
39+
doc.getContents().regexpCapture("(?s).*@id (\\w+)/([\\w\\-]+)\\s.*", 2) // query ID (without lang)
40+
+ "-" + doc.getContents().regexpCapture("(?s).*@kind (\\w+)\\s.*", 1) // @kind
41+
+ "-" +
42+
strictcount(String e | e.getParent*() = sel.getExpr(any(int i | i % 2 = 1))) // the number of string constants in the select
43+
+ "-" + count(sel.getExpr(_)) // and the total number of expressions in the select
44+
)
45+
}
46+
47+
/**
48+
* Gets information about the select.
49+
* The query-id (without language), the language, the message from the select, and a language agnostic fingerprint.
50+
*/
51+
Select parseSelect(string id, string lang, string msg, string fingerPrint) {
52+
exists(File file, QLDoc doc | result.getLocation().getFile() = file |
53+
any(TopLevel top | top.getLocation().getFile() = file).getQLDoc() = doc and
54+
id = doc.getContents().regexpCapture("(?s).*@id (\\w+)/([\\w\\-]+)\\s.*", 2) and
55+
lang = doc.getContents().regexpCapture("(?s).*@id (\\w+)/([\\w\\-]+)\\s.*", 1) and
56+
fingerPrint = getSelectFingerPrint(result) and
57+
msg = getMessage(result).toLowerCase() // case normalize, because some languages upper-case methods.
58+
) and
59+
// excluding experimental
60+
not result.getLocation().getFile().getRelativePath().matches("%/experimental/%") and
61+
not lang = "ql" // excluding QL-for-QL
62+
}
63+
64+
from Select sel, string id, string lang, string msg, string fingerPrint, string badLangs
65+
where
66+
// for a select with a fingerprint
67+
sel = parseSelect(id, lang, msg, fingerPrint) and
68+
// there exists other languages with the same fingerprint, but other message
69+
badLangs =
70+
strictconcat(string bad |
71+
bad != lang and
72+
exists(parseSelect(id, bad, any(string otherMsg | otherMsg != msg), fingerPrint))
73+
|
74+
bad, ", "
75+
)
76+
select sel,
77+
"The " + lang + "/" + id + " query does not have the same alert message as " + badLangs + "."

0 commit comments

Comments
 (0)