blob: 36b397f59f169eeea7f7af63a493fbe78deda77e [file] [log] [blame] [raw]
// Copyright (c) 2019, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
const BaseCompiler = require('../base-compiler'),
argumentParsers = require("./argument-parsers"),
fs = require('fs-extra'),
utils = require('../utils.js'),
path = require('path'),
logger = require('../logger').logger;
class JavaCompiler extends BaseCompiler {
constructor(compilerInfo, env) {
// Default is to disable all "cosmetic" filters
if (!compilerInfo.disabledFilters) {
compilerInfo.disabledFilters = ['labels', 'directives', 'commentOnly', 'trim'];
}
super(compilerInfo, env);
}
async objdump(outputFilename, result, maxSize) {
const dirPath = path.dirname(outputFilename);
const files = await fs.readdir(dirPath);
logger.verbose("Class files: ", files);
const results = await Promise.all(files.filter(f => f.endsWith('.class')).map(async classFile => {
const args = [
// Prints out disassembled code, i.e., the instructions that comprise the Java bytecodes,
// for each of the methods in the class.
'-c',
// Prints out line and local variable tables.
'-l',
classFile
];
const objResult = await this.exec(this.compiler.objdumper, args, {maxOutput: maxSize, customCwd: dirPath});
const oneResult = {};
oneResult.asm = objResult.stdout;
if (objResult.code !== 0) {
oneResult.asm = "<No output: javap returned " + objResult.code + ">";
}
return oneResult;
}));
const merged = {asm: []};
for (const result of results) {
const asmBackup = merged.asm;
Object.assign(merged, result);
merged.asm = asmBackup;
merged.asm.push(result.asm);
}
result.asm = merged.asm;
return result;
}
optionsForFilter(filters) {
// Forcibly enable javap
filters.binary = true;
return [
'-Xlint:all'
];
}
getArgumentParser() {
return argumentParsers.Java;
}
getOutputFilename(dirPath) {
return path.join(dirPath, `${path.basename(this.compileFilename, this.lang.extensions[0])}.class`);
}
filterUserOptions(userOptions) {
const filteredOptions = [];
let toSkip = 0;
const oneArgBlacklist = [
// -d directory
// Sets the destination directory for class files.
"-d",
// -s directory
// Specifies the directory used to place the generated source files.
"-s",
// --source-path path or -sourcepath path
// Specifies where to find input source files.
"--source-path", "-sourcepath"];
for (const userOption of userOptions) {
if (toSkip > 0) {
toSkip--;
continue;
}
if (oneArgBlacklist.indexOf(userOption) !== -1) {
toSkip = 1;
continue;
}
filteredOptions.push(userOption);
}
return filteredOptions;
}
processAsm(result) {
// Handle "error" documents.
if (result.asm.indexOf("\n") === -1 && result.asm[0] === '<') {
return [{text: result.asm, source: null}];
}
// result.asm is an array of javap stdouts
const parseds = result.asm.map(asm => this.parseAsmForClass(asm));
// Sort class file outputs according to first source line they reference
parseds.sort((o1, o2) => o1.firstSourceLine - o2.firstSourceLine);
const segments = [];
parseds.forEach((parsed, classNumber) => {
if (classNumber > 0) {
// Separate classes with two line breaks
segments.push({text: "", source: null});
segments.push({text: "", source: null});
}
for (let i = 0; i < parsed.textsBeforeMethod.length; i++) {
// Line-based highlighting doesn't work if some segments span multiple lines,
// even if they don't have a source line number
// -> split the lines and create segment for each separately
for (const line of utils.splitLines(parsed.textsBeforeMethod[i])) {
// javap output always starts with "Compiled from" on first line, discard these lines.
if (line.startsWith("Compiled from")) {
continue;
}
segments.push({
text: line,
source: null
});
}
// textsBeforeMethod[last] is actually *after* the last method.
// Check whether there is a method following the text block
if (i < parsed.methods.length) {
parsed.methods[i].instructions.forEach(({text, sourceLine}) => {
segments.push({text: text, source: {file: null, line: sourceLine}});
});
}
}
});
return segments;
}
parseAsmForClass(javapOut) {
const textsBeforeMethod = [];
const methods = [];
// javap output puts ` Code:` after every signature. (Line will not be shown to user)
// We use this to find the individual methods.
// Before the first `Code:` occurrence, there is the method signature as well as the name of the class.
// Subsequent matches are always followed by lists of assembly instructions as well as line info mappings
// Regex idea: make sure `Code:` is the only thing on the line. Also consume trailing line ending!
const [classNameAndFirstMethodSignature, ...codeAndLineNumberTables] = javapOut.split(/^\s+Code:\s*$\r?\n/m);
textsBeforeMethod.push(classNameAndFirstMethodSignature.trimEnd()); // possible trailing \r on windows
for (const codeAndLineNumberTable of codeAndLineNumberTables) {
const method = {
instructions: []
};
methods.push(method);
for (const codeLineCandidate of utils.splitLines(codeAndLineNumberTable)) {
// Match
// 1: invokespecial #1 // Method java/lang/Object."<init>":()V
const match = codeLineCandidate.match(/\s+(\d+): (.*)/);
if (match) {
const instrOffset = Number.parseInt(match[1]);
method.instructions.push({
instrOffset: instrOffset,
// Should an instruction ever not be followed by a line number table,
// it might contain a trailing \r on Windows -> trim it, otherwise this would not be necessary
text: codeLineCandidate.trimRight()
});
} else {
break;
}
}
let lineRegex = /line\s*(\d+):\s*(\d+)/g;
let m;
let currentInstr = 0;
let currentSourceLine = -1;
let lastIndex = -1;
do {
m = lineRegex.exec(codeAndLineNumberTable);
if (m) {
// If exec doesn't find a match anymore, lineRegex.lastIndex will be reset to 0
// therefore, cache value here on match
lastIndex = lineRegex.lastIndex;
const [, sourceLineS, instructionS] = m;
logger.verbose("Found source mapping: ", sourceLineS, "to instruction", instructionS);
const instrOffset = Number.parseInt(instructionS);
// Some instructions don't receive an explicit line number.
// They are all assigned to the previous explicit line number,
// because the line consists of multiple instructions.
while (currentInstr < method.instructions.length &&
method.instructions[currentInstr].instrOffset !== instrOffset) {
if (currentSourceLine !== -1) {
// instructions without explicit line number get assigned the last explicit/same line number
method.instructions[currentInstr].sourceLine = currentSourceLine;
} else {
logger.error("Skipping over instruction even though currentSourceLine == -1");
}
currentInstr++;
}
const sourceLine = Number.parseInt(sourceLineS);
currentSourceLine = sourceLine;
method.instructions[currentInstr].sourceLine = currentSourceLine;
if (typeof method.startLine === "undefined") {
method.startLine = sourceLine;
}
// method.instructions.push({sourceLine: instrOffset});
}
} while (m);
if (lastIndex !== -1) {
// Get "interesting" text after the LineNumbers table (header of next method/tail of file)
// trimRight() because of trailing \r on Windows
textsBeforeMethod.push(codeAndLineNumberTable.substr(lastIndex).trimRight());
}
if (currentSourceLine !== -1) {
// Assign remaining instructions to the same source line
while (currentInstr + 1 < method.instructions.length) {
currentInstr++;
method.instructions[currentInstr].sourceLine = currentSourceLine;
}
}
}
return {
// Used for sorting
firstSourceLine: methods.reduce((p, m) => p === -1 ? m.startLine : Math.min(p, m.startLine), -1),
methods: methods,
textsBeforeMethod
};
}
}
module.exports = JavaCompiler;