Skip to content

Commit 95c027b

Browse files
committed
Add incorrect-date-format-pattern.ql
1 parent 807d778 commit 95c027b

File tree

1 file changed

+117
-0
lines changed

1 file changed

+117
-0
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* Finds creation of a `SimpleDateFormat` or a `DateTimeFormatter` with a format pattern
3+
* which likely does not behave as desired.
4+
*
5+
* @id TODO
6+
* @kind problem
7+
*/
8+
9+
import java
10+
import semmle.code.java.dataflow.DataFlow
11+
12+
class TypeSimpleDateFormat extends Class {
13+
TypeSimpleDateFormat() { hasQualifiedName("java.text", "SimpleDateFormat") }
14+
}
15+
16+
class DateFormatPatternUsage extends Expr {
17+
DateFormatPatternUsage() {
18+
exists(ConstructorCall c |
19+
c.getConstructedType() instanceof TypeSimpleDateFormat and
20+
this = c.getArgument(0) and
21+
c.getConstructor().getParameterType(0) instanceof TypeString
22+
)
23+
or
24+
exists(MethodAccess c, Method m | m = c.getMethod() |
25+
m.getDeclaringType() instanceof TypeSimpleDateFormat and
26+
m.hasName(["applyLocalizedPattern", "applyPattern"]) and
27+
this = c.getArgument(0)
28+
)
29+
or
30+
exists(MethodAccess c, Method m | m = c.getMethod() |
31+
m.getDeclaringType().hasQualifiedName("java.time.format", "DateTimeFormatter") and
32+
m.hasName(["ofLocalizedPattern", "ofPattern"]) and
33+
this = c.getArgument(0)
34+
)
35+
or
36+
exists(MethodAccess c, Method m | m = c.getMethod() |
37+
m.getDeclaringType().hasQualifiedName("java.time.format", "DateTimeFormatterBuilder") and
38+
m.hasName(["appendPattern", "appendLocalized", "getLocalizedDateTimePattern"]) and
39+
this = c.getArgument(0) and
40+
m.getParameterType(0) instanceof TypeString
41+
)
42+
}
43+
}
44+
45+
bindingset[pattern]
46+
string getAPatternIssue(string pattern) {
47+
exists(string s, int index |
48+
index = pattern.indexOf(s) and
49+
// Ignore if substring is escaped by being enclosed in `'...'`
50+
// Note: Currently does not detect if `'` is escaped or if it belongs to a previous literal section,
51+
// but probably good enough for now
52+
not exists(int startIndex, int endIndex |
53+
startIndex < index and endIndex >= index + s.length()
54+
|
55+
pattern.charAt(startIndex) = "'" and
56+
pattern.charAt(endIndex) = "'"
57+
)
58+
|
59+
// See https://errorprone.info/bugpattern/MisusedWeekYear or https://rules.sonarsource.com/java/type/Bug/RSPEC-3986
60+
s = "Y" and result = "`Y` is 'week year', probably meant `y`"
61+
or
62+
s = "D" and
63+
exists(pattern.indexOf(["L", "M"])) and
64+
result = "`D` is 'day in year', probably meant `d` for 'day in month'"
65+
or
66+
s = "h" and
67+
not exists(pattern.indexOf("a")) and
68+
result =
69+
"`h` is 'hour in am/pm (1-12)', probably meant `H` ('hour in day (0-23)') or should include am/pm marker `a`"
70+
or
71+
// `m` ('minute') accidentally used for 'month'
72+
(
73+
s = ["-mm-", ".mm."] and
74+
not pattern.charAt(index - 1) = ["H", "h", "K", "k"]
75+
or
76+
exists(string sep | sep = ["", " ", ".", ":", "-", "_"] |
77+
s = sep + "m" and
78+
// `u` here is for DateTimeFormatter 'year'
79+
pattern.charAt(index - 1) = ["u", "y", "Y", "d", "D"]
80+
or
81+
s = "m" + sep and
82+
pattern.charAt(index + s.length()) = ["u", "y", "Y", "d", "D"]
83+
)
84+
) and
85+
result = "`m` is 'minute in hour', probably meant `M` for 'month in year'"
86+
or
87+
// `M` ('month') accidentally used for 'minute'
88+
exists(string sep | sep = ["", " ", ":", "-", "_"] |
89+
s = sep + "M" and
90+
pattern.charAt(index - 1) = ["H", "h", "K", "k"]
91+
or
92+
s = "M" + sep and pattern.charAt(index + s.length()) = "s"
93+
) and
94+
result = "`M` is 'month in year', probably meant `m` for 'minute in hour'"
95+
or
96+
// `S` ('millisecond') accidentally used for 'second'
97+
s = ["", " ", ":", "-", "_"] + "S" and
98+
pattern.charAt(index - 1) = "m" and
99+
result = "`S` is 'millisecond', probably meant `s` for 'second in minute'"
100+
)
101+
}
102+
103+
from CompileTimeConstantExpr patternStr, DateFormatPatternUsage usage
104+
where
105+
DataFlow::localExprFlow(patternStr, usage) and
106+
// If constant variable value is used, don't report the variable read here, instead report the assigned variable value (see below)
107+
not (
108+
patternStr = usage and
109+
patternStr.(RValue).getVariable().fromSource()
110+
)
111+
or
112+
not DataFlow::localExprFlow(patternStr, usage) and
113+
exists(Variable var |
114+
var.getAnAssignedValue() = patternStr and
115+
DataFlow::localExprFlow(var.getAnAccess(), usage)
116+
)
117+
select patternStr, getAPatternIssue(patternStr.getStringValue())

0 commit comments

Comments
 (0)