JDK-8204976 : CC metric - 88% line code coverage
  • Type: Sub-task
  • Component: tools
  • Sub-Component: javac
  • Priority: P4
  • Status: Closed
  • Resolution: Fixed
  • Submitted: 2018-06-13
  • Updated: 2018-06-13
  • Resolved: 2018-06-13
Description
Coverage rate of diff is 88
changeset:   50453:f91927a2c8d3
user:        jjg
date:        Thu Jun 07 16:06:49 2018 -0700
summary:     8201274: Launch Single-File Source-Code Programs

  com/sun/tools/javac/launcher/Main.java: Main.void main(java.lang.String[])
+    119 |            new Main(System.err).run(VM.getRuntimeArguments(), args);
+    120 |        } catch (Fault f) {
+    121 |            System.err.println(f.getMessage());
+    122 |            System.exit(1);
-    123 |        } catch (InvocationTargetException e) {
    124 |            // leave VM to handle the stacktrace, in the standard manner
-    125 |            throw e.getTargetException();
+    126 |        }
+    127 |    }
  com/sun/tools/javac/launcher/Main.java: Main.void <init>(java.io.PrintStream)
+    139 |        this(new PrintWriter(new OutputStreamWriter(out), true));
+    140 |    }
  com/sun/tools/javac/launcher/Main.java: Main.void <init>(java.io.PrintWriter)
+    148 |    public Main(PrintWriter out) {
+    149 |        this.out = out;
+    150 |    }
    151 |
    152 |    /**
    153 |     * Compiles a source file, and executes the main method it contains.
    154 |     *
    155 |     * <p>The first entry in the {@code args} array is the source file
    156 |     * to be compiled and run; all subsequent entries are passed as
    157 |     * arguments to the main method of the first class found in the file.
    158 |     *
    159 |     * <p>Options for {@code javac} are obtained by filtering the runtime arguments.
    160 |     *
    161 |     * <p>If the main method throws an exception, it will be propagated in an
    162 |     * {@code InvocationTargetException}. In that case, the stack trace of the
    163 |     * target exception will be truncated such that the main method will be the
    164 |     * last entry on the stack. In other words, the stack frames leading up to the
    165 |     * invocation of the main method will be removed.
    166 |     *
    167 |     * @param runtimeArgs the runtime arguments
    168 |     * @param args the arguments
    169 |     * @throws Fault if a problem is detected before the main method can be executed
    170 |     * @throws InvocationTargetException if the main method throws an exception
    171 |     */
    172 |    public void run(String[] runtimeArgs, String[] args) throws Fault, InvocationTargetException {
+    173 |        Path file = getFile(args);
    174 |
+    175 |        Context context = new Context();
+    176 |        String mainClassName = compile(file, getJavacOpts(runtimeArgs), context);
    177 |
+    178 |        String[] appArgs = Arrays.copyOfRange(args, 1, args.length);
+    179 |        execute(mainClassName, appArgs, context);
+    180 |    }
    181 |
    182 |    /**
    183 |     * Returns the path for the filename found in the first of an array of arguments.
    184 |     *
    185 |     * @param args the array
    186 |     * @return the path
    187 |     * @throws Fault if there is a problem determining the path, or if the file does not exist
    188 |     */
    189 |    private Path getFile(String[] args) throws Fault {
+    190 |        if (args.length == 0) {
    191 |            // should not happen when invoked from launcher
-    192 |            throw new Fault(Errors.NoArgs);
    193 |        }
    194 |        Path file;
    195 |        try {
+    196 |            file = Paths.get(args[0]);
-    197 |        } catch (InvalidPathException e) {
-    198 |            throw new Fault(Errors.InvalidFilename(args[0]));
+    199 |        }
+    200 |        if (!Files.exists(file)) {
    201 |            // should not happen when invoked from launcher
-    202 |            throw new Fault(Errors.FileNotFound(file));
    203 |        }
+    204 |        return file;
    205 |    }
    206 |
    207 |    /**
    208 |     * Reads a source file, ignoring the first line if it is not a Java source file and
    209 |     * it begins with {@code #!}.
    210 |     *
    211 |     * <p>If it is not a Java source file, and if the first two bytes are {@code #!},
    212 |     * indicating a "magic number" of an executable text file, the rest of the first line
    213 |     * up to but not including the newline is ignored. All characters after the first two are
    214 |     * read in the {@link Charset#defaultCharset default platform encoding}.
    215 |     *
    216 |     * @param file the file
    217 |     * @return a file object containing the content of the file
    218 |     * @throws Fault if an error occurs while reading the file
    219 |     */
    220 |    private JavaFileObject readFile(Path file) throws Fault {
    221 |        // use a BufferedInputStream to guarantee that we can use mark and reset.
+    222 |        try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(file))) {
    223 |            boolean ignoreFirstLine;
+    224 |            if (file.getFileName().toString().endsWith(".java")) {
+    225 |                ignoreFirstLine = false;
    226 |            } else {
+    227 |                in.mark(2);
+    228 |                ignoreFirstLine = (in.read() == '#') && (in.read() == '!');
+    229 |                if (!ignoreFirstLine) {
-    230 |                    in.reset();
    231 |                }
    232 |            }
+    233 |            try (BufferedReader r = new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) {
+    234 |                StringBuilder sb = new StringBuilder();
+    235 |                if (ignoreFirstLine) {
+    236 |                    r.readLine();
+    237 |                    sb.append("\n"); // preserve line numbers
    238 |                }
+    239 |                char[] buf = new char[1024];
    240 |                int n;
+    241 |                while ((n = r.read(buf, 0, buf.length)) != -1)  {
+    242 |                    sb.append(buf, 0, n);
    243 |                }
+    244 |                return new SimpleJavaFileObject(file.toUri(), SOURCE) {
    245 |                    @Override
    246 |                    public String getName() {
+    247 |                        return file.toString();
    248 |                    }
    249 |                    @Override
    250 |                    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
+    251 |                        return sb;
    252 |                    }
    253 |                    @Override
    254 |                    public boolean isNameCompatible(String simpleName, JavaFileObject.Kind kind) {
    255 |                        // reject package-info and module-info; accept other names
+    256 |                        return (kind == JavaFileObject.Kind.SOURCE)
+    257 |                                && SourceVersion.isIdentifier(simpleName);
    258 |                    }
    259 |                    @Override
    260 |                    public String toString() {
-    261 |                        return "JavacSourceLauncher[" + file + "]";
    262 |                    }
    263 |                };
+    264 |            }
+    265 |        } catch (IOException e) {
-    266 |            throw new Fault(Errors.CantReadFile(file, e));
    267 |        }
    268 |    }
    269 |
    270 |    /**
    271 |     * Returns the subset of the runtime arguments that are relevant to {@code javac}.
    272 |     * Generally, the relevant options are those for setting paths and for configuring the
    273 |     * module system.
    274 |     *
    275 |     * @param runtimeArgs the runtime arguments
    276 |     * @return the subset of the runtime arguments
    277 |     **/
    278 |    private List<String> getJavacOpts(String... runtimeArgs) throws Fault {
+    279 |        List<String> javacOpts = new ArrayList<>();
    280 |
+    281 |        String sourceOpt = System.getProperty("jdk.internal.javac.source");
+    282 |        if (sourceOpt != null) {
+    283 |            Source source = Source.lookup(sourceOpt);
+    284 |            if (source == null) {
+    285 |                throw new Fault(Errors.InvalidValueForSource(sourceOpt));
    286 |            }
+    287 |            javacOpts.addAll(List.of("--release", sourceOpt));
    288 |        }
    289 |
+    290 |        for (int i = 0; i < runtimeArgs.length; i++) {
+    291 |            String arg = runtimeArgs[i];
+    292 |            String opt = arg, value = null;
+    293 |            if (arg.startsWith("--")) {
+    294 |                int eq = arg.indexOf('=');
+    295 |                if (eq > 0) {
+    296 |                    opt = arg.substring(0, eq);
+    297 |                    value = arg.substring(eq + 1);
    298 |                }
    299 |            }
+    300 |            switch (opt) {
    301 |                // The following options all expect a value, either in the following
    302 |                // position, or after '=', for options beginning "--".
    303 |                case "--class-path": case "-classpath": case "-cp":
    304 |                case "--module-path": case "-p":
    305 |                case "--add-exports":
    306 |                case "--add-modules":
    307 |                case "--limit-modules":
    308 |                case "--patch-module":
    309 |                case "--upgrade-module-path":
+    310 |                    if (value == null) {
+    311 |                        if (i== runtimeArgs.length - 1) {
    312 |                            // should not happen when invoked from launcher
-    313 |                            throw new Fault(Errors.NoValueForOption(opt));
    314 |                        }
+    315 |                        value = runtimeArgs[++i];
    316 |                    }
+    317 |                    if (opt.equals("--add-modules") && value.equals("ALL-DEFAULT")) {
    318 |                        // this option is only supported at run time;
    319 |                        // it is not required or supported at compile time
+    320 |                        break;
    321 |                    }
+    322 |                    javacOpts.add(opt);
+    323 |                    javacOpts.add(value);
+    324 |                    break;
    325 |                case "--enable-preview":
+    326 |                    javacOpts.add(opt);
+    327 |                    if (sourceOpt == null) {
+    328 |                        throw new Fault(Errors.EnablePreviewRequiresSource);
    329 |                    }
    330 |                    break;
    331 |                default:
    332 |                    // ignore all other runtime args
    333 |            }
    334 |        }
    335 |
    336 |        // add implicit options
+    337 |        javacOpts.add("-proc:none");
    338 |
+    339 |        return javacOpts;
    340 |    }
    341 |
    342 |    /**
    343 |     * Compiles a source file, placing the class files in a map in memory.
    344 |     * Any messages generated during compilation will be written to the stream
    345 |     * provided when this object was created.
    346 |     *
    347 |     * @param file the source file
    348 |     * @param javacOpts compilation options for {@code javac}
    349 |     * @param context the context for the compilation
    350 |     * @return the name of the first class found in the source file
    351 |     * @throws Fault if any compilation errors occur, or if no class was found
    352 |     */
    353 |    private String compile(Path file, List<String> javacOpts, Context context) throws Fault {
+    354 |        JavaFileObject fo = readFile(file);
    355 |
+    356 |        JavacTool javaCompiler = JavacTool.create();
+    357 |        StandardJavaFileManager stdFileMgr = javaCompiler.getStandardFileManager(null, null, null);
    358 |        try {
+    359 |            stdFileMgr.setLocation(StandardLocation.SOURCE_PATH, Collections.emptyList());
-    360 |        } catch (IOException e) {
-    361 |            throw new java.lang.Error("unexpected exception from file manager", e);
+    362 |        }
+    363 |        JavaFileManager fm = context.getFileManager(stdFileMgr);
+    364 |        JavacTask t = javaCompiler.getTask(out, fm, null, javacOpts, null, List.of(fo));
+    365 |        MainClassListener l = new MainClassListener(t);
+    366 |        Boolean ok = t.call();
+    367 |        if (!ok) {
+    368 |            throw new Fault(Errors.CompilationFailed);
    369 |        }
+    370 |        if (l.mainClass == null) {
+    371 |            throw new Fault(Errors.NoClass);
    372 |        }
+    373 |        String mainClassName = l.mainClass.getQualifiedName().toString();
+    374 |        return mainClassName;
    375 |    }
    376 |
    377 |    /**
    378 |     * Invokes the {@code main} method of a specified class, using a class loader that
    379 |     * will load recently compiled classes from memory.
    380 |     *
    381 |     * @param mainClassName the class to be executed
    382 |     * @param appArgs the arguments for the {@code main} method
    383 |     * @param context the context for the class to be executed
    384 |     * @throws Fault if there is a problem finding or invoking the {@code main} method
    385 |     * @throws InvocationTargetException if the {@code main} method throws an exception
    386 |     */
    387 |    private void execute(String mainClassName, String[] appArgs, Context context)
    388 |            throws Fault, InvocationTargetException {
+    389 |        ClassLoader cl = context.getClassLoader(ClassLoader.getSystemClassLoader());
    390 |        try {
+    391 |            Class<?> appClass = Class.forName(mainClassName, true, cl);
+    392 |            if (appClass.getClassLoader() != cl) {
+    393 |                throw new Fault(Errors.UnexpectedClass(mainClassName));
    394 |            }
+    395 |            Method main = appClass.getDeclaredMethod("main", String[].class);
+    396 |            int PUBLIC_STATIC = Modifier.PUBLIC | Modifier.STATIC;
+    397 |            if ((main.getModifiers() & PUBLIC_STATIC) != PUBLIC_STATIC) {
+    398 |                throw new Fault(Errors.MainNotPublicStatic);
    399 |            }
+    400 |            if (!main.getReturnType().equals(void.class)) {
+    401 |                throw new Fault(Errors.MainNotVoid);
    402 |            }
+    403 |            main.setAccessible(true);
+    404 |            main.invoke(0, (Object) appArgs);
-    405 |        } catch (ClassNotFoundException e) {
-    406 |            throw new Fault(Errors.CantFindClass(mainClassName));
+    407 |        } catch (NoSuchMethodException e) {
+    408 |            throw new Fault(Errors.CantFindMainMethod(mainClassName));
-    409 |        } catch (IllegalAccessException e) {
-    410 |            throw new Fault(Errors.CantAccessMainMethod(mainClassName));
+    411 |        } catch (InvocationTargetException e) {
    412 |            // remove stack frames for source launcher
+    413 |            int invocationFrames = e.getStackTrace().length;
+    414 |            Throwable target = e.getTargetException();
+    415 |            StackTraceElement[] targetTrace = target.getStackTrace();
+    416 |            target.setStackTrace(Arrays.copyOfRange(targetTrace, 0, targetTrace.length - invocationFrames));
+    417 |            throw e;
+    418 |        }
+    419 |    }
    420 |
    421 |    private static final String bundleName = "com.sun.tools.javac.resources.launcher";
+    422 |    private ResourceBundle resourceBundle = null;
  com/sun/tools/javac/launcher/Main.java: Main.java.lang.String getMessage(com.sun.tools.javac.util.JCDiagnostic$Error)
+    432 |        String key = error.key();
+    433 |        Object[] args = error.getArgs();
    434 |        try {
+    435 |            if (resourceBundle == null) {
+    436 |                resourceBundle = ResourceBundle.getBundle(bundleName);
+    437 |                errorPrefix = resourceBundle.getString("launcher.error");
    438 |            }
+    439 |            String resource = resourceBundle.getString(key);
+    440 |            String message = MessageFormat.format(resource, args);
+    441 |            return errorPrefix + message;
-    442 |        } catch (MissingResourceException e) {
-    443 |            return "Cannot access resource; " + key + Arrays.toString(args);
  com/sun/tools/javac/launcher/Main.java: Main.java.lang.String access$000(com.sun.tools.javac.launcher.Main,com.sun.tools.javac.util.JCDiagnostic$Error)
+     88 |public class Main {
  com/sun/tools/javac/launcher/Main.java: Main.void <init>()
+    473 |    private static class Context {
+    474 |        private Map<String, byte[]> inMemoryClasses = new HashMap<>();
  com/sun/tools/javac/launcher/Main.java: Main.javax.tools.JavaFileManager getFileManager(javax.tools.StandardJavaFileManager)
+    477 |            return new MemoryFileManager(inMemoryClasses, delegate);
  com/sun/tools/javac/launcher/Main.java: Main.java.lang.ClassLoader getClassLoader(java.lang.ClassLoader)
+    481 |            return new MemoryClassLoader(inMemoryClasses, parent);
  com/sun/tools/javac/launcher/Main.java: Main.void <init>(com.sun.tools.javac.launcher.Main,com.sun.tools.javac.util.JCDiagnostic$Error)
+     94 |        Fault(Error error) {
+     95 |            super(Main.this.getMessage(error));
+     96 |        }
  com/sun/tools/javac/launcher/Main.java: Main.void <init>(com.sun.source.util.JavacTask)
+    453 |        MainClassListener(JavacTask t) {
+    454 |            t.addTaskListener(this);
+    455 |        }
  com/sun/tools/javac/launcher/Main.java: Main.void started(com.sun.source.util.TaskEvent)
+    459 |            if (ev.getKind() == TaskEvent.Kind.ANALYZE && mainClass == null) {
+    460 |                TypeElement te = ev.getTypeElement();
+    461 |                if (te.getNestingKind() == NestingKind.TOP_LEVEL) {
+    462 |                    mainClass = te;
    463 |                }
    464 |            }
+    465 |        }
  com/sun/tools/javac/launcher/Main.java: Main.void <init>(java.util.Map,java.lang.ClassLoader)
+    537 |            super(parent);
+    538 |            this.map = map;
+    539 |        }
  com/sun/tools/javac/launcher/Main.java: Main.java.lang.Class findClass(java.lang.String)
+    543 |            byte[] bytes = map.get(name);
+    544 |            if (bytes == null) {
-    545 |                throw new ClassNotFoundException(name);
    546 |            }
+    547 |            return defineClass(name, bytes, 0, bytes.length);
  com/sun/tools/javac/launcher/Main.java: Main.void <init>(java.util.Map,javax.tools.JavaFileManager)
+    496 |            super(delegate);
+    497 |            this.map = map;
+    498 |        }
  com/sun/tools/javac/launcher/Main.java: Main.javax.tools.JavaFileObject getJavaFileForOutput(javax.tools.JavaFileManager$Location,java.lang.String,javax.tools.JavaFileObject$Kind,javax.tools.FileObject)
+    503 |            if (location == StandardLocation.CLASS_OUTPUT && kind == JavaFileObject.Kind.CLASS) {
+    504 |                return createInMemoryClassFile(className);
    505 |            } else {
-    506 |                return super.getJavaFileForOutput(location, className, kind, sibling);
  com/sun/tools/javac/launcher/Main.java: Main.javax.tools.JavaFileObject createInMemoryClassFile(java.lang.String)
+    511 |            URI uri = URI.create("memory:///" + className.replace('.', '/') + ".class");
+    512 |            return new SimpleJavaFileObject(uri, JavaFileObject.Kind.CLASS) {
  com/sun/tools/javac/launcher/Main.java: Main.java.util.Map access$200(com.sun.tools.javac.launcher.Main$MemoryFileManager)
+    492 |    private static class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
  com/sun/tools/javac/launcher/Main.java: Main.java.io.OutputStream openOutputStream()
+    515 |                    return new ByteArrayOutputStream() {
  com/sun/tools/javac/launcher/Main.java: Main.void close()
+    518 |                            super.close();
+    519 |                            map.put(className, toByteArray());
+    520 |                        }
  sun/launcher/LauncherHelper.java: LauncherHelper.java.lang.Class checkAndLoadMain(boolean,int,java.lang.String)
+    548 |        Class<?> mainClass = null;
+    549 |        switch (mode) {
    550 |            case LM_MODULE: case LM_SOURCE:
+    551 |                mainClass = loadModuleMainClass(what);
+    552 |                break;
    553 |            default:
+    554 |                mainClass = loadMainClass(mode, what);
    555 |                break;
    556 |        }
  sun/launcher/LauncherHelper.java: LauncherHelper.void <clinit>()
    505 |    //   enum LaunchMode { LM_UNKNOWN = 0, LM_CLASS, LM_JAR, LM_MODULE, LM_SOURCE }
    511 |    private static final int LM_SOURCE  = 4;
    542 |    @SuppressWarnings("fallthrough")
lines: 563 new; 157 covered; 20 not covered; 386 not code; 0 no information