replaced <code></code> with more concise {@code}
[idea/community.git] / java / compiler / javac2 / src / com / intellij / ant / Javac2.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
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.intellij.ant;
17
18 import com.intellij.compiler.instrumentation.FailSafeClassReader;
19 import com.intellij.compiler.instrumentation.InstrumentationClassFinder;
20 import com.intellij.compiler.instrumentation.InstrumenterClassWriter;
21 import com.intellij.compiler.notNullVerification.NotNullVerifyingInstrumenter;
22 import com.intellij.uiDesigner.compiler.*;
23 import com.intellij.uiDesigner.lw.CompiledClassPropertiesProvider;
24 import com.intellij.uiDesigner.lw.LwRootContainer;
25 import org.apache.tools.ant.BuildException;
26 import org.apache.tools.ant.Project;
27 import org.apache.tools.ant.taskdefs.Javac;
28 import org.apache.tools.ant.types.Path;
29 import org.apache.tools.ant.util.regexp.Regexp;
30 import org.jetbrains.org.objectweb.asm.*;
31
32 import java.io.*;
33 import java.net.MalformedURLException;
34 import java.net.URL;
35 import java.util.*;
36
37 public class Javac2 extends Javac {
38   public static final String PROPERTY_INSTRUMENTATION_INCLUDE_JAVA_RUNTIME = "javac2.instrumentation.includeJavaRuntime";
39   private ArrayList myFormFiles;
40   private List myNestedFormPathList;
41   private boolean instrumentNotNull = true;
42   private String myNotNullAnnotations = "org.jetbrains.annotations.NotNull";
43   private List<Regexp> myClassFilterAnnotationRegexpList = new ArrayList<Regexp>(0);
44
45   public Javac2() {
46   }
47
48   /**
49    * Check if Java classes should be actually compiled by the task. This method is overridden by
50    * {@link com.intellij.ant.InstrumentIdeaExtensions} task in order to suppress actual compilation
51    * of the java sources.
52    *
53    * @return true if the java classes are compiled, false if just instrumentation is performed.
54    */
55   protected boolean areJavaClassesCompiled() {
56     return true;
57   }
58
59   /**
60    * This method is called when option that supported only for the case when java sources are compiled
61    * and it is not supported for the case when only instrumentation is performed.
62    *
63    * @param optionName the option name to warn about.
64    */
65   private void unsupportedOptionMessage(final String optionName) {
66     if (!areJavaClassesCompiled()) {
67       log("The option " + optionName + " is not supported by InstrumentIdeaExtensions task", Project.MSG_ERR);
68     }
69   }
70
71   public boolean getInstrumentNotNull() {
72     return instrumentNotNull;
73   }
74
75   public void setInstrumentNotNull(boolean instrumentNotNull) {
76     this.instrumentNotNull = instrumentNotNull;
77   }
78
79   /**
80    * @return semicolon-separated names of not-null annotations to be instrumented. Example: {@code "org.jetbrains.annotations.NotNull;javax.annotation.Nonnull"}
81    */
82   public String getNotNullAnnotations() {
83     return myNotNullAnnotations;
84   }
85
86   /**
87    * @param notNullAnnotations semicolon-separated names of not-null annotations to be instrumented. Example: {@code "org.jetbrains.annotations.NotNull;javax.annotation.Nonnull"}
88    */
89   public void setNotNullAnnotations(String notNullAnnotations) {
90     myNotNullAnnotations = notNullAnnotations;
91   }
92
93   /**
94    * Allows to specify patterns of annotation class names to skip NotNull instrumentation on classes which have at least one
95    * annotation matching at least one of the given patterns
96    *
97    * @param regexp the regular expression for JVM internal name (slash-separated) of annotations
98    */
99   public void add(final ClassFilterAnnotationRegexp regexp) {
100     myClassFilterAnnotationRegexpList.add(regexp.getRegexp(getProject()));
101   }
102
103   /**
104    * The overridden setter method that warns about unsupported option.
105    *
106    * @param v the option value
107    */
108   public void setDebugLevel(String v) {
109     unsupportedOptionMessage("debugLevel");
110     super.setDebugLevel(v);
111   }
112
113   /**
114    * The overridden setter method that warns about unsupported option.
115    *
116    * @param list the option value
117    */
118   public void setListfiles(boolean list) {
119     unsupportedOptionMessage("listFiles");
120     super.setListfiles(list);
121   }
122
123   /**
124    * The overridden setter method that warns about unsupported option.
125    *
126    * @param memoryInitialSize the option value
127    */
128   public void setMemoryInitialSize(String memoryInitialSize) {
129     unsupportedOptionMessage("memoryInitialSize");
130     super.setMemoryInitialSize(memoryInitialSize);
131   }
132
133   /**
134    * The overridden setter method that warns about unsupported option.
135    *
136    * @param memoryMaximumSize the option value
137    */
138   public void setMemoryMaximumSize(String memoryMaximumSize) {
139     unsupportedOptionMessage("memoryMaximumSize");
140     super.setMemoryMaximumSize(memoryMaximumSize);
141   }
142
143   /**
144    * The overridden setter method that warns about unsupported option.
145    *
146    * @param encoding the option value
147    */
148   public void setEncoding(String encoding) {
149     unsupportedOptionMessage("encoding");
150     super.setEncoding(encoding);
151   }
152
153   /**
154    * The overridden setter method that warns about unsupported option.
155    *
156    * @param optimize the option value
157    */
158   public void setOptimize(boolean optimize) {
159     unsupportedOptionMessage("optimize");
160     super.setOptimize(optimize);
161   }
162
163   /**
164    * The overridden setter method that warns about unsupported option.
165    *
166    * @param depend the option value
167    */
168   public void setDepend(boolean depend) {
169     unsupportedOptionMessage("depend");
170     super.setDepend(depend);
171   }
172
173   /**
174    * The overridden setter method that warns about unsupported option.
175    *
176    * @param f the option value
177    */
178   public void setFork(boolean f) {
179     unsupportedOptionMessage("fork");
180     super.setFork(f);
181   }
182
183   /**
184    * The overridden setter method that warns about unsupported option.
185    *
186    * @param forkExec the option value
187    */
188   public void setExecutable(String forkExec) {
189     unsupportedOptionMessage("executable");
190     super.setExecutable(forkExec);
191   }
192
193   /**
194    * The overridden setter method that warns about unsupported option.
195    *
196    * @param compiler the option value
197    */
198   public void setCompiler(String compiler) {
199     unsupportedOptionMessage("compiler");
200     super.setCompiler(compiler);
201   }
202
203   /**
204    * Sets the nested form directories that will be used during the
205    * compilation.
206    * @param nestedformdirs a list of {@link PrefixedPath}
207    */
208   public void setNestedformdirs(List nestedformdirs) {
209     myNestedFormPathList = nestedformdirs;
210   }
211
212   /**
213    * Gets the nested form directories that will be used during the
214    * compilation.
215    * @return the extension directories as a list of {@link PrefixedPath}
216    */
217   public List getNestedformdirs() {
218     return myNestedFormPathList;
219   }
220
221   /**
222    * Adds a path to nested form directories.
223    * @return a path to be configured
224    */
225   public PrefixedPath createNestedformdirs() {
226     PrefixedPath p = new PrefixedPath(getProject());
227     if (myNestedFormPathList == null) {
228       myNestedFormPathList = new ArrayList();
229     }
230     myNestedFormPathList.add(p);
231     return p;
232   }
233
234
235
236   /**
237    * The overridden compile method that does not actually compiles java sources but only instruments
238    * class files.
239    */
240   protected void compile() {
241     // compile java
242     if (areJavaClassesCompiled()) {
243       super.compile();
244     }
245
246     InstrumentationClassFinder finder = buildClasspathClassLoader();
247     if (finder == null) {
248       return;
249     }
250     try {
251       instrumentForms(finder);
252
253       if (getInstrumentNotNull()) {
254         //NotNull instrumentation
255         final int instrumented = instrumentNotNull(getDestdir(), finder);
256         log("Added @NotNull assertions to " + instrumented + " files", Project.MSG_INFO);
257       }
258     }
259     finally {
260       finder.releaseResources();
261     }
262   }
263
264   /**
265    * Instrument forms
266    *
267    * @param finder a classloader to use
268    */
269   private void instrumentForms(final InstrumentationClassFinder finder) {
270     // we instrument every file, because we cannot find which files should not be instrumented without dependency storage
271     final ArrayList formsToInstrument = myFormFiles;
272
273     if (formsToInstrument.size() == 0) {
274       log("No forms to instrument found", Project.MSG_VERBOSE);
275       return;
276     }
277
278     final HashMap class2form = new HashMap();
279
280     for (int i = 0; i < formsToInstrument.size(); i++) {
281       final File formFile = (File)formsToInstrument.get(i);
282
283       log("compiling form " + formFile.getAbsolutePath(), Project.MSG_VERBOSE);
284       final LwRootContainer rootContainer;
285       try {
286         rootContainer = Utils.getRootContainer(formFile.toURI().toURL(), new CompiledClassPropertiesProvider(finder.getLoader()));
287       }
288       catch (AlienFormFileException e) {
289         // ignore non-IDEA forms
290         continue;
291       }
292       catch (Exception e) {
293         fireError("Cannot process form file " + formFile.getAbsolutePath() + ". Reason: " + e);
294         continue;
295       }
296
297       final String classToBind = rootContainer.getClassToBind();
298       if (classToBind == null) {
299         continue;
300       }
301
302       String name = classToBind.replace('.', '/');
303       File classFile = getClassFile(name);
304       if (classFile == null) {
305         log(formFile.getAbsolutePath() + ": Class to bind does not exist: " + classToBind, Project.MSG_WARN);
306         continue;
307       }
308
309       final File alreadyProcessedForm = (File)class2form.get(classToBind);
310       if (alreadyProcessedForm != null) {
311         fireError(formFile.getAbsolutePath() +
312                   ": " +
313                   "The form is bound to the class " +
314                   classToBind +
315                   ".\n" +
316                   "Another form " +
317                   alreadyProcessedForm.getAbsolutePath() +
318                   " is also bound to this class.");
319         continue;
320       }
321       class2form.put(classToBind, formFile);
322
323       try {
324         int version;
325         InputStream stream = new FileInputStream(classFile);
326         try {
327           version = getClassFileVersion(new ClassReader(stream));
328         }
329         finally {
330           stream.close();
331         }
332         AntNestedFormLoader formLoader = new AntNestedFormLoader(finder.getLoader(), myNestedFormPathList);
333         InstrumenterClassWriter classWriter = new InstrumenterClassWriter(getAsmClassWriterFlags(version), finder);
334         final AsmCodeGenerator codeGenerator = new AsmCodeGenerator(rootContainer, finder, formLoader, false, classWriter);
335         codeGenerator.patchFile(classFile);
336         final FormErrorInfo[] warnings = codeGenerator.getWarnings();
337
338         for (int j = 0; j < warnings.length; j++) {
339           log(formFile.getAbsolutePath() + ": " + warnings[j].getErrorMessage(), Project.MSG_WARN);
340         }
341         final FormErrorInfo[] errors = codeGenerator.getErrors();
342         if (errors.length > 0) {
343           StringBuffer message = new StringBuffer();
344           for (int j = 0; j < errors.length; j++) {
345             if (message.length() > 0) {
346               message.append("\n");
347             }
348             message.append(formFile.getAbsolutePath()).append(": ").append(errors[j].getErrorMessage());
349           }
350           fireError(message.toString());
351         }
352       }
353       catch (Exception e) {
354         fireError("Forms instrumentation failed for " + formFile.getAbsolutePath() + ": " + e.toString());
355       }
356     }
357   }
358
359   /**
360    * @return the flags for class writer
361    */
362   private static int getAsmClassWriterFlags(int version) {
363     return version >= Opcodes.V1_6 && version != Opcodes.V1_1 ? ClassWriter.COMPUTE_FRAMES : ClassWriter.COMPUTE_MAXS;
364   }
365
366   /**
367    * Create class loader based on classpath, bootclasspath, and sourcepath.
368    *
369    * @return a URL classloader
370    */
371   private InstrumentationClassFinder buildClasspathClassLoader() {
372     final StringBuffer classPathBuffer = new StringBuffer();
373     final Project project = getProject();
374     final Path cp = new Path(project);
375     appendPath(cp, getBootclasspath());
376     cp.setLocation(getDestdir().getAbsoluteFile());
377     appendPath(cp, getClasspath());
378     appendPath(cp, getSourcepath());
379     appendPath(cp, getSrcdir());
380     if (getIncludeantruntime()) {
381       cp.addExisting(cp.concatSystemClasspath("last"));
382     }
383     boolean shouldInclude = getIncludejavaruntime();
384     if (!shouldInclude) {
385       if (project != null) {
386         final String propValue = project.getProperty(PROPERTY_INSTRUMENTATION_INCLUDE_JAVA_RUNTIME);
387         shouldInclude = !("false".equalsIgnoreCase(propValue) || "no".equalsIgnoreCase(propValue));
388       }
389       else {
390         shouldInclude = true;
391       }
392     }
393     if (shouldInclude) {
394       cp.addJavaRuntime();
395     }
396
397     cp.addExtdirs(getExtdirs());
398
399     final String[] pathElements = cp.list();
400     for (int i = 0; i < pathElements.length; i++) {
401       final String pathElement = pathElements[i];
402       classPathBuffer.append(File.pathSeparator);
403       classPathBuffer.append(pathElement);
404     }
405
406     final String classPath = classPathBuffer.toString();
407     log("classpath=" + classPath, Project.MSG_VERBOSE);
408
409     try {
410       return createInstrumentationClassFinder(classPath);
411     }
412     catch (MalformedURLException e) {
413       fireError(e.getMessage());
414       return null;
415     }
416   }
417
418   /**
419    * Append path to class path if the appened path is not empty and is not null
420    *
421    * @param cp the path to modify
422    * @param p  the path to append
423    */
424   private void appendPath(Path cp, final Path p) {
425     if (p != null && p.size() > 0) {
426       cp.append(p);
427     }
428   }
429
430   /**
431    * Instrument classes with NotNull annotations
432    *
433    * @param dir    the directory with classes to instrument (the directory is processed recursively)
434    * @param finder the classloader to use
435    * @return the amount of classes actually affected by instrumentation
436    */
437   private int instrumentNotNull(File dir, final InstrumentationClassFinder finder) {
438     int instrumented = 0;
439     final File[] files = dir.listFiles();
440     for (int i = 0; i < files.length; i++) {
441       File file = files[i];
442       final String name = file.getName();
443       if (name.endsWith(".class")) {
444         final String path = file.getPath();
445         log("Adding @NotNull assertions to " + path, Project.MSG_VERBOSE);
446         try {
447           final FileInputStream inputStream = new FileInputStream(file);
448           try {
449             FailSafeClassReader reader = new FailSafeClassReader(inputStream);
450
451             int version = getClassFileVersion(reader);
452             
453             if (version >= Opcodes.V1_5 && !shouldBeSkippedByAnnotationPattern(reader)) {
454               ClassWriter writer = new InstrumenterClassWriter(reader, getAsmClassWriterFlags(version), finder);
455
456               if (NotNullVerifyingInstrumenter.processClassFile(reader, writer, myNotNullAnnotations.split(";"))) {
457                 final FileOutputStream fileOutputStream = new FileOutputStream(path);
458                 try {
459                   fileOutputStream.write(writer.toByteArray());
460                   instrumented++;
461                 }
462                 finally {
463                   fileOutputStream.close();
464                 }
465               }
466             }
467           }
468           finally {
469             inputStream.close();
470           }
471         }
472         catch (IOException e) {
473           log("Failed to instrument @NotNull assertion for " + path + ": " + e.getMessage(), Project.MSG_WARN);
474         }
475         catch (Exception e) {
476           fireError("@NotNull instrumentation failed for " + path + ": " + e.toString());
477         }
478       }
479       else if (file.isDirectory()) {
480         instrumented += instrumentNotNull(file, finder);
481       }
482     }
483
484     return instrumented;
485   }
486
487   private static int getClassFileVersion(ClassReader reader) {
488     final int[] classfileVersion = new int[1];
489     reader.accept(new ClassVisitor(Opcodes.API_VERSION) {
490       public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
491         classfileVersion[0] = version;
492       }
493     }, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
494
495     return classfileVersion[0];
496   }
497
498   private boolean shouldBeSkippedByAnnotationPattern(ClassReader reader) {
499     if (myClassFilterAnnotationRegexpList.isEmpty()) {
500       return false;
501     }
502
503     final boolean[] result = new boolean[]{false};
504     reader.accept(new ClassVisitor(Opcodes.API_VERSION) {
505       public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
506         if (!result[0]) {
507           String internalName = Type.getType(desc).getInternalName();
508           for (Regexp regexp : myClassFilterAnnotationRegexpList) {
509             if (regexp.matches(internalName)) {
510               result[0] = true;
511               break;
512             }
513           }
514         }
515         return null;
516       }
517     }, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
518
519     return result[0];
520   }
521
522   private void fireError(final String message) {
523     if (failOnError) {
524       throw new BuildException(message, getLocation());
525     }
526     else {
527       log(message, Project.MSG_ERR);
528     }
529   }
530
531   private File getClassFile(String className) {
532     final String classOrInnerName = getClassOrInnerName(className);
533     if (classOrInnerName == null) return null;
534     return new File(getDestdir().getAbsolutePath(), classOrInnerName + ".class");
535   }
536
537   private String getClassOrInnerName(String className) {
538     File classFile = new File(getDestdir().getAbsolutePath(), className + ".class");
539     if (classFile.exists()) return className;
540     int position = className.lastIndexOf('/');
541     if (position == -1) return null;
542     return getClassOrInnerName(className.substring(0, position) + '$' + className.substring(position + 1));
543   }
544
545   protected void resetFileLists() {
546     super.resetFileLists();
547     myFormFiles = new ArrayList();
548   }
549
550   protected void scanDir(final File srcDir, final File destDir, final String[] files) {
551     super.scanDir(srcDir, destDir, files);
552     for (int i = 0; i < files.length; i++) {
553       final String file = files[i];
554       if (file.endsWith(".form")) {
555         log("Found form file " + file, Project.MSG_VERBOSE);
556         myFormFiles.add(new File(srcDir, file));
557       }
558     }
559   }
560
561   private static InstrumentationClassFinder createInstrumentationClassFinder(final String classPath) throws MalformedURLException {
562     final ArrayList urls = new ArrayList();
563     for (StringTokenizer tokenizer = new StringTokenizer(classPath, File.pathSeparator); tokenizer.hasMoreTokens();) {
564       final String s = tokenizer.nextToken();
565       urls.add(new File(s).toURI().toURL());
566     }
567     final URL[] urlsArr = (URL[])urls.toArray(new URL[urls.size()]);
568     return new InstrumentationClassFinder(urlsArr);
569   }
570
571   private class AntNestedFormLoader implements NestedFormLoader {
572     private final ClassLoader myLoader;
573     private final List myNestedFormPathList;
574     private final HashMap myFormCache = new HashMap();
575
576     public AntNestedFormLoader(final ClassLoader loader, List nestedFormPathList) {
577       myLoader = loader;
578       myNestedFormPathList = nestedFormPathList;
579     }
580
581     public LwRootContainer loadForm(String formFilePath) throws Exception {
582       if (myFormCache.containsKey(formFilePath)) {
583         return (LwRootContainer)myFormCache.get(formFilePath);
584       }
585
586       String lowerFormFilePath = formFilePath.toLowerCase();
587       log("Searching for form " + lowerFormFilePath, Project.MSG_VERBOSE);
588       for (Iterator iterator = myFormFiles.iterator(); iterator.hasNext();) {
589         File file = (File)iterator.next();
590         String name = file.getAbsolutePath().replace(File.separatorChar, '/').toLowerCase();
591         log("Comparing with " + name, Project.MSG_VERBOSE);
592         if (name.endsWith(lowerFormFilePath)) {
593           return loadForm(formFilePath, new FileInputStream(file));
594         }
595       }
596
597       if (myNestedFormPathList != null) {
598         for (int i = 0; i < myNestedFormPathList.size(); i++) {
599           PrefixedPath path = (PrefixedPath)myNestedFormPathList.get(i);
600           File formFile = path.findFile(formFilePath);
601           if (formFile != null) {
602             return loadForm(formFilePath, new FileInputStream(formFile));
603           }
604         }
605       }
606       InputStream resourceStream = myLoader.getResourceAsStream(formFilePath);
607       if (resourceStream != null) {
608         return loadForm(formFilePath, resourceStream);
609       }
610       throw new Exception("Cannot find nested form file " + formFilePath);
611     }
612
613     private LwRootContainer loadForm(String formFileName, InputStream resourceStream) throws Exception {
614       final LwRootContainer container = Utils.getRootContainer(resourceStream, null);
615       myFormCache.put(formFileName, container);
616       return container;
617     }
618
619     public String getClassToBindName(LwRootContainer container) {
620       final String className = container.getClassToBind();
621       String result = getClassOrInnerName(className.replace('.', '/'));
622       if (result != null) return result.replace('/', '.');
623       return className;
624     }
625   }
626 }