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