]> WPIA git - gigi.git/blob - util-testing/club/wpia/gigi/localisation/SQLTestingVisitor.java
add: code to statically verify SQL call patterns
[gigi.git] / util-testing / club / wpia / gigi / localisation / SQLTestingVisitor.java
1 package club.wpia.gigi.localisation;
2
3 import java.sql.Date;
4 import java.sql.ParameterMetaData;
5 import java.sql.SQLException;
6 import java.sql.Timestamp;
7 import java.sql.Types;
8 import java.util.Arrays;
9 import java.util.Deque;
10 import java.util.LinkedList;
11 import java.util.List;
12 import java.util.concurrent.LinkedBlockingDeque;
13
14 import org.eclipse.jdt.internal.compiler.ASTVisitor;
15 import org.eclipse.jdt.internal.compiler.CompilationResult;
16 import org.eclipse.jdt.internal.compiler.ast.AllocationExpression;
17 import org.eclipse.jdt.internal.compiler.ast.BinaryExpression;
18 import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
19 import org.eclipse.jdt.internal.compiler.ast.ConditionalExpression;
20 import org.eclipse.jdt.internal.compiler.ast.Expression;
21 import org.eclipse.jdt.internal.compiler.ast.ExtendedStringLiteral;
22 import org.eclipse.jdt.internal.compiler.ast.IntLiteral;
23 import org.eclipse.jdt.internal.compiler.ast.MessageSend;
24 import org.eclipse.jdt.internal.compiler.ast.SingleNameReference;
25 import org.eclipse.jdt.internal.compiler.ast.StringLiteral;
26 import org.eclipse.jdt.internal.compiler.ast.TryStatement;
27 import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
28 import org.eclipse.jdt.internal.compiler.impl.Constant;
29 import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
30 import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
31 import org.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope;
32 import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
33 import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;
34 import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
35
36 import club.wpia.gigi.database.DBEnum;
37 import club.wpia.gigi.database.DatabaseConnection;
38 import club.wpia.gigi.database.GigiPreparedStatement;
39
40 public class SQLTestingVisitor extends ASTVisitor {
41
42     private CompilationUnitDeclaration pu;
43
44     private TranslationCollector tc;
45
46     private enum Type {
47         TIMESTAMP(Types.TIMESTAMP), ENUM(Types.VARCHAR), STRING(Types.VARCHAR), BOOLEAN(Types.BOOLEAN), DATE(Types.DATE), INTEGER(Types.INTEGER), OTHER(0);
48
49         private final int sqltype;
50
51         private Type(int sqltype) {
52             this.sqltype = sqltype;
53
54         }
55
56         public void set(GigiPreparedStatement ps, int index) {
57             if (this == TIMESTAMP) {
58                 ps.setTimestamp(index, new Timestamp(System.currentTimeMillis()));
59             } else if (this == STRING) {
60                 ps.setString(index, "y");
61             } else if (this == DATE) {
62                 ps.setDate(index, new Date(System.currentTimeMillis()));
63             } else if (this == Type.BOOLEAN) {
64                 ps.setBoolean(index, false);
65             } else if (this == OTHER || this == INTEGER) {
66                 ps.setInt(index, 1000);
67             } else {
68                 throw new Error();
69             }
70         }
71
72         public boolean isOfSQLType(int i) {
73             if (i == sqltype) {
74                 return true;
75             }
76             if (i == Types.BIT && this == BOOLEAN) {
77                 return true;
78             }
79             return false;
80         }
81     }
82
83     private class TypeInstantiation {
84
85         Type type;
86
87         String enumValue;
88
89         public TypeInstantiation(Type type) {
90             this.type = type;
91         }
92
93         public TypeInstantiation(Type type, String enumValue) {
94             this.enumValue = enumValue;
95             this.type = type;
96         }
97
98         public void set(GigiPreparedStatement ps, int index) {
99             if (type == Type.ENUM) {
100                 ps.setString(index, enumValue);
101             } else {
102                 type.set(ps, index);
103             }
104         }
105
106         @Override
107         public String toString() {
108             return type.toString() + (enumValue != null ? enumValue : "");
109         }
110
111         @Override
112         public int hashCode() {
113             final int prime = 31;
114             int result = 1;
115             result = prime * result + getOuterType().hashCode();
116             result = prime * result + ((enumValue == null) ? 0 : enumValue.hashCode());
117             result = prime * result + ((type == null) ? 0 : type.hashCode());
118             return result;
119         }
120
121         @Override
122         public boolean equals(Object obj) {
123             if (this == obj) {
124                 return true;
125             }
126             if (obj == null) {
127                 return false;
128             }
129             if (getClass() != obj.getClass()) {
130                 return false;
131             }
132             TypeInstantiation other = (TypeInstantiation) obj;
133             if ( !getOuterType().equals(other.getOuterType())) {
134                 return false;
135             }
136             if (enumValue == null) {
137                 if (other.enumValue != null) {
138                     return false;
139                 }
140             } else if ( !enumValue.equals(other.enumValue)) {
141                 return false;
142             }
143             if (type != other.type) {
144                 return false;
145             }
146             return true;
147         }
148
149         private SQLTestingVisitor getOuterType() {
150             return SQLTestingVisitor.this;
151         }
152
153     }
154
155     public class SQLOccurrence {
156
157         private List<String> query;
158
159         private TryStatement target;
160
161         private CompilationResult source;
162
163         public TypeInstantiation[] types = new TypeInstantiation[10];
164
165         private int sourceStart;
166
167         public SQLOccurrence(TryStatement target) {
168             this.target = target;
169         }
170
171         public void setQuery(List<String> query, CompilationResult compilationResult, int sourceStart) {
172             this.query = query;
173             this.source = compilationResult;
174             this.sourceStart = sourceStart;
175         }
176
177         public TryStatement getTarget() {
178             return target;
179         }
180
181         public List<String> getQuery() {
182             return query;
183         }
184
185         public int getSourceStart() {
186             return sourceStart;
187         }
188
189         public boolean isQuery() {
190             return query != null;
191         }
192
193         public String getPosition() {
194             int pos = source.lineSeparatorPositions.length + 1;
195             for (int i = 0; i < source.lineSeparatorPositions.length; i++) {
196                 if (source.lineSeparatorPositions[i] > sourceStart) {
197                     pos = i + 1;
198                     break;
199                 }
200             }
201             return new String(source.getFileName()) + ":" + pos;
202         }
203
204         private void check(String stmt) {
205             tc.countStatement();
206             try (DatabaseConnection.Link l = DatabaseConnection.newLink(true)) {
207                 try (GigiPreparedStatement ps = new GigiPreparedStatement(stmt)) {
208                     ParameterMetaData dt = ps.getParameterMetaData();
209                     int count = dt.getParameterCount();
210                     for (int i = 1; i <= types.length; i++) {
211                         if (i > count) {
212                             if (types[i - 1] != null) {
213                                 errMsg(stmt, "too many params");
214                                 return;
215                             }
216                             continue;
217                         }
218                         int tp = dt.getParameterType(i);
219                         TypeInstantiation t = types[i - 1];
220                         if (t == null) {
221                             errMsg(stmt, "arg " + i + " not set");
222                             return;
223                         }
224                         if ( !t.type.isOfSQLType(tp)) {
225                             errMsg(stmt, "type mismatch. From parameter setting code: " + t + ", in SQL statement: " + tp);
226                             return;
227                         }
228                     }
229                 } catch (SQLException e) {
230                     throw new Error(e);
231                 }
232             } catch (InterruptedException e) {
233                 e.printStackTrace();
234             }
235         }
236
237         private void errMsg(String stmt, String errMsg) {
238             System.err.println(getPosition());
239             System.err.println("Problem with statement: " + stmt);
240             System.err.println(Arrays.toString(types));
241             System.err.println(errMsg);
242             tc.hadError();
243         }
244
245         public void check() {
246             for (String q : getQuery()) {
247                 check(q);
248             }
249
250         }
251     }
252
253     public SQLTestingVisitor(CompilationUnitDeclaration pu, TranslationCollector tc) {
254         this.pu = pu;
255         this.tc = tc;
256     }
257
258     Deque<SQLOccurrence> ts = new LinkedBlockingDeque<>();
259
260     @Override
261     public boolean visit(TypeDeclaration typeDeclaration, CompilationUnitScope scope) {
262         return true;
263     }
264
265     @Override
266     public boolean visit(TypeDeclaration memberTypeDeclaration, ClassScope scope) {
267         return true;
268     }
269
270     @Override
271     public boolean visit(TryStatement tryStatement, BlockScope scope) {
272         ts.push(new SQLOccurrence(tryStatement));
273         return true;
274     }
275
276     @Override
277     public void endVisit(TryStatement tryStatement, BlockScope scope) {
278         SQLOccurrence occ = ts.pop();
279         if (occ.isQuery()) {
280             occ.check();
281         }
282         if (occ.getTarget() != tryStatement) {
283             throw new Error();
284         }
285     }
286
287     @Override
288     public boolean visit(AllocationExpression ae, BlockScope scope) {
289         MethodBinding mb = ae.binding;
290         if (new String(mb.declaringClass.qualifiedPackageName()).equals("club.wpia.gigi.database") && new String(mb.declaringClass.qualifiedSourceName()).equals("GigiPreparedStatement")) {
291             String sig = new String(mb.readableName());
292             if (sig.equals("GigiPreparedStatement(String)") || sig.equals("GigiPreparedStatement(String, boolean)")) {
293                 List<String> l = getQueries(ae.arguments[0], scope);
294                 if (l.size() == 0) {
295                     return false;
296                 }
297                 LinkedList<String> qs = new LinkedList<>();
298                 for (String q : l) {
299                     qs.add(DatabaseConnection.preprocessQuery(q));
300                 }
301                 ts.peek().setQuery(qs, scope.compilationUnitScope().referenceContext.compilationResult, ae.sourceStart);
302             } else {
303                 throw new Error(sig);
304             }
305         }
306         return true;
307     }
308
309     private List<String> getQueries(Expression q, BlockScope scope) {
310         SourceTypeBinding typ = scope.enclosingSourceType();
311         String fullType = new String(typ.qualifiedPackageName()) + "." + new String(typ.qualifiedSourceName());
312         if (fullType.equals("club.wpia.gigi.database.IntegrityVerifier")) {
313             return Arrays.asList();
314         }
315         if (q instanceof StringLiteral) {
316             String s = new String(((StringLiteral) q).source());
317             return Arrays.asList(s);
318         } else if (q instanceof ExtendedStringLiteral) {
319             throw new Error();
320         } else if (q instanceof BinaryExpression) {
321             Expression l = ((BinaryExpression) q).left;
322             Expression r = ((BinaryExpression) q).right;
323             if ( !((BinaryExpression) q).operatorToString().equals("+")) {
324                 throw new Error(((BinaryExpression) q).operatorToString());
325             }
326             List<String> left = getQueries(l, scope);
327             List<String> right = getQueries(r, scope);
328             LinkedList<String> res = new LinkedList<>();
329             for (String leftS : left) {
330                 for (String rightS : right) {
331                     res.add(leftS + rightS);
332                 }
333             }
334             return res;
335         } else if (q instanceof ConditionalExpression) {
336             Expression t = ((ConditionalExpression) q).valueIfTrue;
337             Expression f = ((ConditionalExpression) q).valueIfFalse;
338             List<String> res = new LinkedList<>();
339             res.addAll(getQueries(t, scope));
340             res.addAll(getQueries(f, scope));
341             return res;
342         } else if (q instanceof SingleNameReference) {
343             SingleNameReference ref = (SingleNameReference) q;
344             Constant c = ref.constant;
345             if (c.equals(Constant.NotAConstant)) {
346                 throw new Error(q.toString());
347             }
348             return Arrays.asList(c.stringValue());
349         } else {
350             System.err.println(q.getClass() + ";" + q.toString());
351             throw new Error(q.toString());
352         }
353     }
354
355     @Override
356     public boolean visit(MessageSend messageSend, BlockScope scope) {
357         Expression r = messageSend.receiver;
358         String rec = new String(r.resolvedType.readableName());
359         if (rec.equals("club.wpia.gigi.database.GigiPreparedStatement")) {
360             String selector = new String(messageSend.selector);
361             if (selector.startsWith("set")) {
362                 SQLOccurrence peek = ts.peek();
363                 if (peek == null) {
364                     throw new Error("setting parameter at bad location");
365                 }
366                 IntLiteral i = (IntLiteral) messageSend.arguments[0];
367                 int val = i.constant.intValue();
368                 TypeInstantiation typeInstantiation = getTypeInstantiation(messageSend, selector);
369                 if (peek.types[val - 1] != null && !peek.types[val - 1].equals(typeInstantiation)) {
370                     throw new Error("multiple different typeInstantiations");
371                 }
372                 peek.types[val - 1] = typeInstantiation;
373             }
374         }
375         return true;
376     }
377
378     private TypeInstantiation getTypeInstantiation(MessageSend messageSend, String selector) throws Error {
379         switch (selector) {
380         case "setTimestamp":
381             return new TypeInstantiation(Type.TIMESTAMP);
382         case "setEnum":
383             TypeBinding rn = messageSend.arguments[1].resolvedType;
384             String sn = new String(rn.qualifiedSourceName());
385             String enumClass = new String(rn.readableName());
386             enumClass = enumClass.substring(0, enumClass.length() - sn.length()) + sn.replace('.', '$');
387             String dbn;
388             try {
389                 dbn = ((DBEnum) ((Object[]) Class.forName(enumClass).getMethod("values").invoke(null))[0]).getDBName();
390             } catch (ReflectiveOperationException e) {
391                 throw new Error(e);
392             }
393             return new TypeInstantiation(Type.ENUM, dbn);
394         case "setString":
395             return new TypeInstantiation(Type.STRING);
396         case "setDate":
397             return new TypeInstantiation(Type.DATE);
398         case "setInt":
399             return new TypeInstantiation(Type.INTEGER);
400         case "setBoolean":
401             return new TypeInstantiation(Type.BOOLEAN);
402         default:
403             return new TypeInstantiation(Type.OTHER);
404         }
405     }
406 }