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.RuleSet; 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 private Map<String, String> jdtOptions; 59 private Writer reportWriter; 60 Main(boolean debug)61 public Main(boolean debug) { 62 this.debug = debug; 63 } 64 setJdtOptions(Map<String, String> jdtOptions)65 public Main setJdtOptions(Map<String, String> jdtOptions) { 66 this.jdtOptions = jdtOptions; 67 return this; 68 } 69 setReportWriter(Writer reportWriter)70 public Main setReportWriter(Writer reportWriter) { 71 this.reportWriter = reportWriter; 72 return this; 73 } 74 execute(RuleSet ruleSet)75 public void execute(RuleSet ruleSet) throws Exception { 76 if (reportWriter == null) { 77 reportWriter = new OutputStreamWriter(System.out); 78 } 79 80 Map<String, String> compilerOptions = this.jdtOptions; 81 if (compilerOptions == null) { 82 compilerOptions = defaultJdtOptions(); 83 } 84 ASTParser parser = ASTParser.newParser(AST.JLS8); 85 parser.setCompilerOptions(compilerOptions); 86 parser.setKind(ASTParser.K_COMPILATION_UNIT); 87 88 InputFileGenerator inputFileGenerator = ruleSet.getInputFileGenerator(); 89 OutputSourceFileGenerator outputSourceFileGenerator = ruleSet.getOutputSourceFileGenerator(); 90 for (File inputFile : inputFileGenerator.generate()) { 91 System.out.println("Processing: " + inputFile); 92 93 String source = readSource(inputFile); 94 CompilationUnitHandler compilationUnitHandler = new CompilationUnitHandler( 95 inputFile, parser, source, new PrintWriter(reportWriter), compilerOptions 96 ); 97 compilationUnitHandler.setDebug(debug); 98 99 List<Rule> ruleList = ruleSet.getRuleList(inputFile); 100 if (!ruleList.isEmpty()) { 101 for (Rule rule : ruleList) { 102 compilationUnitHandler.apply(rule); 103 } 104 } 105 106 File outputFile = outputSourceFileGenerator.generate( 107 compilationUnitHandler.getCompilationUnit(), inputFile); 108 if (outputFile != null) { 109 writeSource(compilationUnitHandler.getDocument(), outputFile); 110 } 111 } 112 } 113 writeSource(Document document, File outputFile)114 private static void writeSource(Document document, File outputFile) throws IOException { 115 File outputDir = outputFile.getParentFile(); 116 if (outputDir.exists()) { 117 if (!outputDir.isDirectory()) { 118 throw new IOException(outputDir + " is not a directory"); 119 } 120 } 121 if (!outputDir.exists()) { 122 if (!outputDir.mkdirs()) { 123 throw new IOException("Unable to create " + outputDir); 124 } 125 } 126 String source = document.get(); 127 128 // TODO Look at guava for this 129 FileOutputStream fos = new FileOutputStream(outputFile); 130 Writer writer = new OutputStreamWriter(fos, JAVA_SOURCE_CHARSET); 131 try { 132 writer.write(source.toCharArray()); 133 } finally { 134 writer.close(); 135 } 136 } 137 readSource(File file)138 private static String readSource(File file) throws IOException { 139 StringBuilder sb = new StringBuilder(2048); 140 try (BufferedReader reader = new BufferedReader( 141 new InputStreamReader(new FileInputStream(file), JAVA_SOURCE_CHARSET))) { 142 char[] buffer = new char[1024]; 143 int count; 144 while ((count = reader.read(buffer)) != -1) { 145 sb.append(buffer, 0, count); 146 } 147 } 148 return sb.toString(); 149 } 150 151 private static class CompilationUnitHandler implements Context { 152 153 private final File file; 154 private final ASTParser parser; 155 private final Reporter reporter; 156 private final Map<String, String> jdtOptions; 157 158 private boolean debug; 159 160 private Document documentBefore; 161 private CompilationUnit compilationUnitBefore; 162 163 private Document documentRequested; 164 private TrackingASTRewrite rewriteRequested; 165 CompilationUnitHandler(File file, ASTParser parser, String source, PrintWriter reportWriter, Map<String, String> jdtOptions)166 public CompilationUnitHandler(File file, ASTParser parser, String source, 167 PrintWriter reportWriter, Map<String, String> jdtOptions) { 168 this.file = file; 169 this.parser = parser; 170 this.reporter = new ReporterImpl(reportWriter); 171 this.jdtOptions = jdtOptions; 172 173 // Initialize source / AST state. 174 documentBefore = new Document(source); 175 compilationUnitBefore = parseDocument(file, parser, documentBefore, jdtOptions); 176 } 177 setDebug(boolean debug)178 public void setDebug(boolean debug) { 179 this.debug = debug; 180 } 181 apply(Rule rule)182 public void apply(Rule rule) throws BadLocationException { 183 if (documentRequested != null || rewriteRequested != null) { 184 throw new AssertionError("Handler state not reset properly"); 185 } 186 187 if (rule.matches(compilationUnitBefore)) { 188 // Apply the rule. 189 rule.process(this, compilationUnitBefore); 190 191 // Work out what happened, report/error as needed and reset the state. 192 CompilationUnit compilationUnitAfter; 193 Document documentAfter; 194 if (ruleUsedRewrite()) { 195 if (debug) { 196 System.out.println("AST processor: " + rule + ", rewrite: " + 197 (rewriteRequested.isEmpty() ? "None" : rewriteRequested.toString())); 198 } 199 if (rewriteRequested.isEmpty()) { 200 if (rule.mustModify()) { 201 throw new RuntimeException("AST processor Rule: " + rule 202 + " did not modify the compilation unit as it should"); 203 } 204 documentAfter = documentBefore; 205 compilationUnitAfter = compilationUnitBefore; 206 } else { 207 Document documentToRewrite = new Document(documentBefore.get()); 208 compilationUnitAfter = applyRewrite(file + " after " + rule, parser, 209 documentToRewrite, rewriteRequested, jdtOptions); 210 documentAfter = documentToRewrite; 211 } 212 } else if (ruleUsedDocument()) { 213 String sourceBefore = documentBefore.get(); 214 String sourceAfter = documentRequested.get(); 215 if (debug) { 216 System.out.println( 217 "Document processor: " + rule + ", diff: " + 218 generateDiff(sourceBefore, sourceAfter)); 219 } 220 if (sourceBefore.equals(sourceAfter)) { 221 if (rule.mustModify()) { 222 throw new RuntimeException("Document processor Rule: " + rule 223 + " did not modify document as it should"); 224 } 225 documentAfter = documentBefore; 226 compilationUnitAfter = compilationUnitBefore; 227 } else { 228 // Regenerate the AST from the modified document. 229 compilationUnitAfter = parseDocument( 230 file + " after document processor " + rule, parser, documentRequested, jdtOptions); 231 documentAfter = documentRequested; 232 } 233 } else { 234 // The rule didn't request anything.... should this be an error? 235 compilationUnitAfter = compilationUnitBefore; 236 documentAfter = documentBefore; 237 } 238 239 // Reset document / compilation state for the next round. 240 documentBefore = documentAfter; 241 compilationUnitBefore = compilationUnitAfter; 242 documentRequested = null; 243 rewriteRequested = null; 244 } 245 } 246 rewrite()247 @Override public ASTRewrite rewrite() { 248 if (documentRequested != null) { 249 throw new IllegalStateException("document() already called."); 250 } 251 if (rewriteRequested != null) { 252 throw new IllegalStateException("rewrite() already called."); 253 } 254 rewriteRequested = createTrackingASTRewrite(compilationUnitBefore); 255 return rewriteRequested; 256 } 257 document()258 @Override public Document document() { 259 if (rewriteRequested != null) { 260 throw new IllegalStateException("rewrite() already called."); 261 } 262 if (documentRequested != null) { 263 throw new IllegalStateException("document() already called."); 264 } 265 documentRequested = new Document(documentBefore.get()); 266 return documentRequested; 267 } 268 reporter()269 @Override public Reporter reporter() { 270 return reporter; 271 } 272 getCompilationUnit()273 public CompilationUnit getCompilationUnit() { 274 return compilationUnitBefore; 275 } 276 getDocument()277 public Document getDocument() { 278 return documentBefore; 279 } 280 ruleUsedRewrite()281 private boolean ruleUsedRewrite() { 282 return rewriteRequested != null; 283 } 284 ruleUsedDocument()285 private boolean ruleUsedDocument() { 286 return documentRequested != null; 287 } 288 applyRewrite(Object documentId, ASTParser parser, Document document, ASTRewrite rewrite, Map<String, String> options)289 private static CompilationUnit applyRewrite(Object documentId, ASTParser parser, 290 Document document, ASTRewrite rewrite, Map<String, String> options) 291 throws BadLocationException { 292 TextEdit textEdit = rewrite.rewriteAST(document, options); 293 textEdit.apply(document, TextEdit.UPDATE_REGIONS); 294 // Reparse the document. 295 return parseDocument(documentId, parser, document, options); 296 } 297 parseDocument(Object documentId, ASTParser parser, Document document, Map<String, String> compilerOptions)298 private static CompilationUnit parseDocument(Object documentId, ASTParser parser, 299 Document document, Map<String, String> compilerOptions) { 300 parser.setSource(document.get().toCharArray()); 301 parser.setCompilerOptions(compilerOptions); 302 303 CompilationUnit cu = (CompilationUnit) parser.createAST(null /* progressMonitor */); 304 if (cu.getProblems().length > 0) { 305 System.err.println("Error parsing:" + documentId + ": " 306 + Arrays.toString(cu.getProblems())); 307 throw new RuntimeException("Unable to parse document. Stopping."); 308 } 309 return cu; 310 } 311 createTrackingASTRewrite(CompilationUnit cu)312 private static TrackingASTRewrite createTrackingASTRewrite(CompilationUnit cu) { 313 return new TrackingASTRewrite(cu.getAST()); 314 } 315 generateDiff(String before, String after)316 private static String generateDiff(String before, String after) { 317 if (before.equals(after)) { 318 return "No diff"; 319 } 320 // TODO Implement this 321 return "Diff. DIFF NOT IMPLEMENTED"; 322 } 323 324 private class ReporterImpl implements Reporter { 325 326 private final PrintWriter reportWriter; 327 ReporterImpl(PrintWriter reportWriter)328 public ReporterImpl(PrintWriter reportWriter) { 329 this.reportWriter = reportWriter; 330 } 331 info(String message)332 @Override public void info(String message) { 333 reportInternal(compilationUnitIdentifier(), message); 334 } 335 336 @Override info(ASTNode node, String message)337 public void info(ASTNode node, String message) { 338 reportInternal(nodeIdentifier(node), message); 339 } 340 reportInternal(String locationIdentifier, String message)341 private void reportInternal(String locationIdentifier, String message) { 342 reportWriter 343 .append(locationIdentifier) 344 .append(": ") 345 .append(message) 346 .append('\n'); 347 } 348 compilationUnitIdentifier()349 private String compilationUnitIdentifier() { 350 return file.getPath(); 351 } 352 nodeIdentifier(ASTNode node)353 private String nodeIdentifier(ASTNode node) { 354 String approximateNodeLocation; 355 try { 356 approximateNodeLocation = "line approx. " + 357 documentBefore.getLineOfOffset(node.getStartPosition()); 358 } catch (BadLocationException e) { 359 approximateNodeLocation = "unknown location"; 360 } 361 return file.getPath() + "(" + approximateNodeLocation + ")"; 362 } 363 } 364 } 365 366 private static class TrackingASTRewrite extends ASTRewrite { 367 TrackingASTRewrite(AST ast)368 public TrackingASTRewrite(AST ast) { 369 super(ast); 370 } 371 isEmpty()372 public boolean isEmpty() { 373 return !getRewriteEventStore().getChangeRootIterator().hasNext(); 374 } 375 } 376 defaultJdtOptions()377 private static Map<String, String> defaultJdtOptions() { 378 Map<String, String> options = JavaCore.getOptions(); 379 options.put(JavaCore.COMPILER_COMPLIANCE, JavaCore.VERSION_1_8); 380 options.put(JavaCore.COMPILER_SOURCE, JavaCore.VERSION_1_8); 381 options.put(JavaCore.COMPILER_DOC_COMMENT_SUPPORT, JavaCore.ENABLED); 382 return options; 383 } 384 } 385