|
| 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