1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.currysrc; 17 18 import com.google.currysrc.api.Rules; 19 import com.google.currysrc.api.input.InputFileGenerator; 20 import com.google.currysrc.api.output.OutputSourceFileGenerator; 21 import com.google.currysrc.api.process.Context; 22 import com.google.currysrc.api.process.Reporter; 23 import com.google.currysrc.api.process.Rule; 24 25 import org.eclipse.jdt.core.JavaCore; 26 import org.eclipse.jdt.core.dom.AST; 27 import org.eclipse.jdt.core.dom.ASTNode; 28 import org.eclipse.jdt.core.dom.ASTParser; 29 import org.eclipse.jdt.core.dom.CompilationUnit; 30 import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; 31 import org.eclipse.jface.text.BadLocationException; 32 import org.eclipse.jface.text.Document; 33 import org.eclipse.text.edits.TextEdit; 34 35 import java.io.BufferedReader; 36 import java.io.File; 37 import java.io.FileInputStream; 38 import java.io.FileOutputStream; 39 import java.io.IOException; 40 import java.io.InputStreamReader; 41 import java.io.OutputStreamWriter; 42 import java.io.PrintWriter; 43 import java.io.Writer; 44 import java.nio.charset.Charset; 45 import java.nio.charset.StandardCharsets; 46 import java.util.Arrays; 47 import java.util.List; 48 import java.util.Map; 49 50 /** 51 * The main execution API for users of currysrc. 52 */ 53 public final class Main { 54 55 private static final Charset JAVA_SOURCE_CHARSET = StandardCharsets.UTF_8; 56 57 private final boolean debug; 58 59 public Main(boolean debug) { 60 this.debug = debug; 61 } 62 63 public void execute(Rules rules) throws Exception { 64 execute(rules, new OutputStreamWriter(System.out)); 65 } 66 67 public void execute(Rules rules, Writer reportWriter) throws Exception { 68 ASTParser parser = ASTParser.newParser(AST.JLS8); 69 parser.setKind(ASTParser.K_COMPILATION_UNIT); 70 71 InputFileGenerator inputFileGenerator = rules.getInputFileGenerator(); 72 OutputSourceFileGenerator outputSourceFileGenerator = rules.getOutputSourceFileGenerator(); 73 for (File inputFile : inputFileGenerator.generate()) { 74 System.out.println("Processing: " + inputFile); 75 76 String source = readSource(inputFile); 77 CompilationUnitHandler compilationUnitHandler = 78 new CompilationUnitHandler(inputFile, parser, source, new PrintWriter(reportWriter)); 79 compilationUnitHandler.setDebug(debug); 80 81 List<Rule> ruleList = rules.getRuleList(inputFile); 82 if (!ruleList.isEmpty()) { 83 for (Rule rule : ruleList) { 84 compilationUnitHandler.apply(rule); 85 } 86 } 87 88 File outputFile = outputSourceFileGenerator.generate( 89 compilationUnitHandler.getCompilationUnit(), inputFile); 90 if (outputFile != null) { 91 writeSource(compilationUnitHandler.getDocument(), outputFile); 92 } 93 } 94 } 95 96 private static void writeSource(Document document, File outputFile) throws IOException { 97 File outputDir = outputFile.getParentFile(); 98 if (outputDir.exists()) { 99 if (!outputDir.isDirectory()) { 100 throw new IOException(outputDir + " is not a directory"); 101 } 102 } 103 if (!outputDir.exists()) { 104 if (!outputDir.mkdirs()) { 105 throw new IOException("Unable to create " + outputDir); 106 } 107 } 108 String source = document.get(); 109 110 // TODO Look at guava for this 111 FileOutputStream fos = new FileOutputStream(outputFile); 112 Writer writer = new OutputStreamWriter(fos, JAVA_SOURCE_CHARSET); 113 try { 114 writer.write(source.toCharArray()); 115 } finally { 116 writer.close(); 117 } 118 } 119 120 private static String readSource(File file) throws IOException { 121 StringBuilder sb = new StringBuilder(2048); 122 try (BufferedReader reader = new BufferedReader( 123 new InputStreamReader(new FileInputStream(file), JAVA_SOURCE_CHARSET))) { 124 char[] buffer = new char[1024]; 125 int count; 126 while ((count = reader.read(buffer)) != -1) { 127 sb.append(buffer, 0, count); 128 } 129 } 130 return sb.toString(); 131 } 132 133 private static class CompilationUnitHandler implements Context { 134 135 private final File file; 136 private final ASTParser parser; 137 private final Reporter reporter; 138 139 private boolean debug; 140 141 private Document documentBefore; 142 private CompilationUnit compilationUnitBefore; 143 144 private Document documentRequested; 145 private TrackingASTRewrite rewriteRequested; 146 147 public CompilationUnitHandler(File file, ASTParser parser, String source, 148 PrintWriter reportWriter) { 149 this.file = file; 150 this.parser = parser; 151 this.reporter = new ReporterImpl(reportWriter); 152 153 // Initialize source / AST state. 154 documentBefore = new Document(source); 155 compilationUnitBefore = parseDocument(file, parser, documentBefore); 156 } 157 158 public void setDebug(boolean debug) { 159 this.debug = debug; 160 } 161 162 public void apply(Rule rule) throws BadLocationException { 163 if (documentRequested != null || rewriteRequested != null) { 164 throw new AssertionError("Handler state not reset properly"); 165 } 166 167 if (rule.matches(compilationUnitBefore)) { 168 // Apply the rule. 169 rule.process(this, compilationUnitBefore); 170 171 // Work out what happened, report/error as needed and reset the state. 172 CompilationUnit compilationUnitAfter; 173 Document documentAfter; 174 if (ruleUsedRewrite()) { 175 if (debug) { 176 System.out.println("AST processor: " + rule + ", rewrite: " + 177 (rewriteRequested.isEmpty() ? "None" : rewriteRequested.toString())); 178 } 179 if (rewriteRequested.isEmpty()) { 180 if (rule.mustModify()) { 181 throw new RuntimeException("AST processor Rule: " + rule 182 + " did not modify the compilation unit as it should"); 183 } 184 documentAfter = documentBefore; 185 compilationUnitAfter = compilationUnitBefore; 186 } else { 187 Document documentToRewrite = new Document(documentBefore.get()); 188 compilationUnitAfter = applyRewrite(file + " after " + rule, parser, 189 documentToRewrite, rewriteRequested); 190 documentAfter = documentToRewrite; 191 } 192 } else if (ruleUsedDocument()) { 193 String sourceBefore = documentBefore.get(); 194 String sourceAfter = documentRequested.get(); 195 if (debug) { 196 System.out.println( 197 "Document processor: " + rule + ", diff: " + 198 generateDiff(sourceBefore, sourceAfter)); 199 } 200 if (sourceBefore.equals(sourceAfter)) { 201 if (rule.mustModify()) { 202 throw new RuntimeException("Document processor Rule: " + rule 203 + " did not modify document as it should"); 204 } 205 documentAfter = documentBefore; 206 compilationUnitAfter = compilationUnitBefore; 207 } else { 208 // Regenerate the AST from the modified document. 209 compilationUnitAfter = parseDocument( 210 file + " after document processor " + rule, parser, documentRequested); 211 documentAfter = documentRequested; 212 } 213 } else { 214 // The rule didn't request anything.... should this be an error? 215 compilationUnitAfter = compilationUnitBefore; 216 documentAfter = documentBefore; 217 } 218 219 // Reset document / compilation state for the next round. 220 documentBefore = documentAfter; 221 compilationUnitBefore = compilationUnitAfter; 222 documentRequested = null; 223 rewriteRequested = null; 224 } 225 } 226 227 @Override public ASTRewrite rewrite() { 228 if (documentRequested != null) { 229 throw new IllegalStateException("document() already called."); 230 } 231 if (rewriteRequested != null) { 232 throw new IllegalStateException("rewrite() already called."); 233 } 234 rewriteRequested = createTrackingASTRewrite(compilationUnitBefore); 235 return rewriteRequested; 236 } 237 238 @Override public Document document() { 239 if (rewriteRequested != null) { 240 throw new IllegalStateException("rewrite() already called."); 241 } 242 if (documentRequested != null) { 243 throw new IllegalStateException("document() already called."); 244 } 245 documentRequested = new Document(documentBefore.get()); 246 return documentRequested; 247 } 248 249 @Override public Reporter reporter() { 250 return reporter; 251 } 252 253 public CompilationUnit getCompilationUnit() { 254 return compilationUnitBefore; 255 } 256 257 public Document getDocument() { 258 return documentBefore; 259 } 260 261 private boolean ruleUsedRewrite() { 262 return rewriteRequested != null; 263 } 264 265 private boolean ruleUsedDocument() { 266 return documentRequested != null; 267 } 268 269 private static CompilationUnit applyRewrite(Object documentId, ASTParser parser, 270 Document document, ASTRewrite rewrite) throws BadLocationException { 271 TextEdit textEdit = rewrite.rewriteAST(document, null); 272 textEdit.apply(document, TextEdit.UPDATE_REGIONS); 273 // Reparse the document. 274 return parseDocument(documentId, parser, document); 275 } 276 277 private static CompilationUnit parseDocument(Object documentId, ASTParser parser, 278 Document document) { 279 parser.setSource(document.get().toCharArray()); 280 configureParser(parser); 281 282 CompilationUnit cu = (CompilationUnit) parser.createAST(null /* progressMonitor */); 283 if (cu.getProblems().length > 0) { 284 System.err.println("Error parsing:" + documentId + ": " + Arrays.toString(cu.getProblems())); 285 throw new RuntimeException("Unable to parse document. Stopping."); 286 } 287 return cu; 288 } 289 290 private static void configureParser(ASTParser parser) { 291 Map<String, String> options = JavaCore.getOptions(); 292 options.put(JavaCore.COMPILER_COMPLIANCE, JavaCore.VERSION_1_7); 293 options.put(JavaCore.COMPILER_SOURCE, JavaCore.VERSION_1_7); 294 options.put(JavaCore.COMPILER_DOC_COMMENT_SUPPORT, JavaCore.ENABLED); 295 parser.setCompilerOptions(options); 296 } 297 298 private static TrackingASTRewrite createTrackingASTRewrite(CompilationUnit cu) { 299 return new TrackingASTRewrite(cu.getAST()); 300 } 301 302 private static String generateDiff(String before, String after) { 303 if (before.equals(after)) { 304 return "No diff"; 305 } 306 // TODO Implement this 307 return "Diff. DIFF NOT IMPLEMENTED"; 308 } 309 310 private class ReporterImpl implements Reporter { 311 312 private final PrintWriter reportWriter; 313 314 public ReporterImpl(PrintWriter reportWriter) { 315 this.reportWriter = reportWriter; 316 } 317 318 @Override public void info(String message) { 319 reportInternal(compilationUnitIdentifier(), message); 320 } 321 322 @Override 323 public void info(ASTNode node, String message) { 324 reportInternal(nodeIdentifier(node), message); 325 } 326 327 private void reportInternal(String locationIdentifier, String message) { 328 reportWriter 329 .append(locationIdentifier) 330 .append(": ") 331 .append(message) 332 .append('\n'); 333 } 334 335 private String compilationUnitIdentifier() { 336 return file.getPath(); 337 } 338 339 private String nodeIdentifier(ASTNode node) { 340 String approximateNodeLocation; 341 try { 342 approximateNodeLocation = "line approx. " + 343 documentBefore.getLineOfOffset(node.getStartPosition()); 344 } catch (BadLocationException e) { 345 approximateNodeLocation = "unknown location"; 346 } 347 return file.getPath() + "(" + approximateNodeLocation + ")"; 348 } 349 } 350 } 351 352 private static class TrackingASTRewrite extends ASTRewrite { 353 354 public TrackingASTRewrite(AST ast) { 355 super(ast); 356 } 357 358 public boolean isEmpty() { 359 return !getRewriteEventStore().getChangeRootIterator().hasNext(); 360 } 361 } 362 } 363