Assembly API documentation for Java Bytecode (#2817)

* Add assembly documentation endpoint for Java bytecode
* Upstream asm docenizer for Java
* Remove jvms.html from gitattributes as it is never checked into vcs
* EOF line for docenizer-java.js
* Use find-node to locate nodejs
* Format code according to style guide
This commit is contained in:
Mats Larsen
2021-08-14 19:56:33 +02:00
committed by GitHub
parent ca017bac31
commit b6c2380c56
12 changed files with 1607 additions and 0 deletions

View File

@@ -8,3 +8,5 @@ views
# Autogenerated files
lib/handlers/asm-docs-amd64.js
lib/handlers/asm-docs-arm32.js
lib/handlers/asm-docs-java.js
etc/scripts/vendor/jvms.html

1
.gitattributes vendored
View File

@@ -3,4 +3,5 @@ docs/* linguist-documentation
*.asm linguist-generated
lib/handlers/asm-docs-amd64.js linguist-generated
lib/handlers/asm-docs-arm32.js linguist-generated
lib/handlers/asm-docs-java.js linguist-generated
test/*-cases/* linguist-generated

View File

@@ -4,6 +4,7 @@ include:
exclude:
- lib/handlers/asm-docs-amd64.js
- lib/handlers/asm-docs-arm32.js
- lib/handlers/asm-docs-java.js
- lib/compilers/fake-for-test.js
report-dir: ./out/coverage
reporter:

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env node
const cheerio = require('cheerio');
const {promises: fs} = require('fs');
const JVMS_SPECIFICATION = './vendor/jvms.html';
const VARIADIC_MAPPINGS = {
'<n>': ['0', '1', '2', '3'],
'<d>': ['0', '1'],
'<f>': ['0, 1', '2'],
'<i>': ['m1', '0', '1', '2', '3', '4', '5'],
'<l>': ['0', '1'],
'<op>': ['g', 'l'],
'<cond>': ['eq', 'ne'],
};
// interface InstructionInfo {
// name: string
// anchor: string
// tooltip: string
// format: string[]
// stack: [string | null, string | null]
// description: string
// }
// extract: (node: cheerio.Cheerio<Element>, $: cheerio.CheerioAPI) => InstructionInfo[]
const extract = (node, $) => {
const anchorElement = node.find('div.titlepage > div > div > h3.title > a[name*="jvms-6.5"]').first();
const nameElement = anchorElement.parent().find('span.emphasis > em');
const anchor = anchorElement.attr('name');
const name = nameElement.text();
const [
operationSection,
formatSection,
_formsSection,
operandStackSection,
descriptionSection,
] = node.find('div.section').toArray().map(it => $(it));
const operation = operationSection.find('p.norm').first().text();
const format = formatSection.find('div.literallayout > p > span.emphasis > em').toArray().map(it => $(it).text());
const description = descriptionSection.find('p.norm-dynamic').first();
// rewrite links to oracle.com
$(description).find('* > a[href*="jvms-"]').toArray().forEach((el) => {
$(el).attr('href', `https://docs.oracle.com/javase/specs/jvms/se16/html/${$(el).attr('href')}`);
});
const [stackBefore, stackAfter] = operandStackSection.find('p.norm')
.toArray()
.map(it => $(it));
// InstructionInfo[]
const result = [];
const hasVariadicMapping = Object.keys(VARIADIC_MAPPINGS).some((pat) => name.endsWith(pat));
if (hasVariadicMapping) {
for (const [pattern, mappings] of Object.entries(VARIADIC_MAPPINGS)) {
if (name.endsWith(pattern)) {
for (const mapping of mappings) {
result.push({
name: name.replace(pattern, mapping).replace('<', '[').replace('>', ']'),
anchor: anchor,
description: description.html(),
tooltip: operation,
stack: [stackBefore?.html(), stackAfter?.html()],
format: format.map(x => x.replace('<', '[').replace('>', ']')),
});
}
}
}
} else {
result.push({
name,
anchor: anchor,
description: description.html(),
tooltip: operation,
stack: [stackBefore?.html(), stackAfter?.html()],
format,
});
}
return result;
};
const main = async () => {
const file = await fs.readFile(JVMS_SPECIFICATION, 'utf-8');
const $ = cheerio.load(file);
const sections = $('div.section-execution');
const instructions = sections.toArray()
.slice(1) // Drop 1 because the first is the "mne monic"
.map(it => extract($(it), $))
.flat();
console.log('export function getAsmOpcode(opcode) {');
console.log(' if (!opcode) return;');
console.log(' switch (opcode.toUpperCase()) {');
for (const instruction of instructions) {
console.log(` case '${instruction.name.toUpperCase()}':`);
console.log(' return {');
console.log(` url: \`https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html#${instruction.anchor}\`,`);
const body = `<p>Instruction ${instruction.name}: ${instruction.tooltip}</p><p>Format: ${instruction.format.join(' ')}</p>${instruction.stack[0] && `<p>Operand Stack: ${instruction.stack[0]} ${instruction.stack[1]}</p>`}<p>${instruction.description}</p>`;
console.log(` html: \`${body.replace(/\s\s+/g, ' ')}\`,`);
console.log(` tooltip: \`${instruction.tooltip.replace(/\s\s+/g, ' ')}\`,`);
console.log(' };');
}
console.log(' }');
console.log('}');
};
main();

8
etc/scripts/docenizerJava.sh Executable file
View File

@@ -0,0 +1,8 @@
#/usr/bin/bash
JVMS_PATH=$(pwd)/vendor/jvms.html
[ -f "$JVMS_PATH" ] || curl https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html -o "$JVMS_PATH"
$(pwd)/find-node ../../.node-bin
$(cat ../../.node-bin) docenizer-java.js > ../../lib/handlers/asm-docs-java.js

0
etc/scripts/vendor/.gitkeep vendored Normal file
View File

View File

@@ -35,6 +35,7 @@ import * as utils from '../utils';
import { AsmDocsHandler as AsmDocsHandlerAarch64 } from './asm-docs-api-aarch64';
import { AsmDocsHandler as AsmDocsHandlerAmd64 } from './asm-docs-api-amd64';
import { AsmDocsHandler as AsmDocsHandlerArm32 } from './asm-docs-api-arm32';
import { AsmDocsHandler as AsmDocsHandlerJava } from './asm-docs-api-java';
import { Formatter } from './formatting';
export class ApiHandler {
@@ -67,6 +68,7 @@ export class ApiHandler {
amd64: new AsmDocsHandlerAmd64(),
arm32: new AsmDocsHandlerArm32(),
aarch64: new AsmDocsHandlerAarch64(),
java: new AsmDocsHandlerJava(),
};
this.handle.get('/asm/:arch/:opcode', (req, res) => {
const handler = asmArch[req.params.arch];

View File

@@ -0,0 +1,50 @@
// Copyright (c) 2021, 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.
import * as props from '../properties';
import { getAsmOpcode } from './asm-docs-java';
export class AsmDocsHandler {
constructor() {
const asmProps = props.propsFor('asm-docs');
this.staticMaxAgeSecs = asmProps('staticMaxAgeSecs', 10);
}
handle(req, res) {
const info = getAsmOpcode(req.params.opcode);
if (this.staticMaxAgeSecs) {
res.setHeader('Cache-Control', `public, max-age=${this.staticMaxAgeSecs}`);
}
if (req.accepts(['text', 'json']) === 'json') {
res.send({found: !!info, result: info});
} else {
if (info) {
res.send(info.html);
} else {
res.send('Unknown opcode');
}
}
}
}

1163
lib/handlers/asm-docs-java.js generated Normal file

File diff suppressed because it is too large Load Diff

185
package-lock.json generated
View File

@@ -3675,6 +3675,117 @@
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"dev": true
},
"cheerio": {
"version": "1.0.0-rc.10",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz",
"integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==",
"dev": true,
"requires": {
"cheerio-select": "^1.5.0",
"dom-serializer": "^1.3.2",
"domhandler": "^4.2.0",
"htmlparser2": "^6.1.0",
"parse5": "^6.0.1",
"parse5-htmlparser2-tree-adapter": "^6.0.1",
"tslib": "^2.2.0"
},
"dependencies": {
"dom-serializer": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz",
"integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==",
"dev": true,
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
"entities": "^2.0.0"
}
},
"domelementtype": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
"dev": true
},
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"dev": true
}
}
},
"cheerio-select": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz",
"integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==",
"dev": true,
"requires": {
"css-select": "^4.1.3",
"css-what": "^5.0.1",
"domelementtype": "^2.2.0",
"domhandler": "^4.2.0",
"domutils": "^2.7.0"
},
"dependencies": {
"css-select": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz",
"integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==",
"dev": true,
"requires": {
"boolbase": "^1.0.0",
"css-what": "^5.0.0",
"domhandler": "^4.2.0",
"domutils": "^2.6.0",
"nth-check": "^2.0.0"
}
},
"css-what": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz",
"integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==",
"dev": true
},
"dom-serializer": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz",
"integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==",
"dev": true,
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
"entities": "^2.0.0"
}
},
"domelementtype": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
"dev": true
},
"domutils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz",
"integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==",
"dev": true,
"requires": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
"domhandler": "^4.2.0"
}
},
"nth-check": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz",
"integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==",
"dev": true,
"requires": {
"boolbase": "^1.0.0"
}
}
}
},
"chokidar": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
@@ -5143,6 +5254,23 @@
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
"dev": true
},
"domhandler": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz",
"integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==",
"dev": true,
"requires": {
"domelementtype": "^2.2.0"
},
"dependencies": {
"domelementtype": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
"dev": true
}
}
},
"domutils": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
@@ -7915,6 +8043,48 @@
}
}
},
"htmlparser2": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
"integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
"dev": true,
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.0.0",
"domutils": "^2.5.2",
"entities": "^2.0.0"
},
"dependencies": {
"dom-serializer": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz",
"integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==",
"dev": true,
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
"entities": "^2.0.0"
}
},
"domelementtype": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
"dev": true
},
"domutils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz",
"integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==",
"dev": true,
"requires": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
"domhandler": "^4.2.0"
}
}
}
},
"http-deceiver": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
@@ -11047,6 +11217,21 @@
"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
"dev": true
},
"parse5": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
"dev": true
},
"parse5-htmlparser2-tree-adapter": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
"integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
"dev": true,
"requires": {
"parse5": "^6.0.1"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",

View File

@@ -91,6 +91,7 @@
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"chai-http": "^4.2.1",
"cheerio": "^1.0.0-rc.10",
"codecov": "^3.8.2",
"copy-webpack-plugin": "^5.1.2",
"css-loader": "^3.6.0",

View File

@@ -0,0 +1,86 @@
// Copyright (c) 2021, 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.
import express from 'express';
import { AsmDocsHandler } from '../../lib/handlers/asm-docs-api-java';
import { chai } from '../utils';
describe('Assembly documents', () => {
let app;
before(() => {
app = express();
const handler = new AsmDocsHandler();
app.use('/asm/:opcode', handler.handle.bind(handler));
});
it('should respond with "unknown opcode" for unknown opcodes', () => {
return chai.request(app).get('/asm/NOTANOPCODE')
.then(res => {
res.should.have.status(200);
res.should.be.html;
res.text.should.equal('Unknown opcode');
})
.catch(err => { throw err; });
});
it('should respond to text requests', () => {
return chai.request(app)
.get('/asm/aaload')
.then(res => {
res.should.have.status(200);
res.should.be.html;
res.text.should.contain('Load reference from array');
})
.catch(err => { throw err; });
});
it('should respond to json requests', () => {
return chai.request(app)
.get('/asm/aaload')
.set('Accept', 'application/json')
.then(res => {
res.should.have.status(200);
res.should.be.json;
res.body.found.should.equal(true);
res.body.result.html.should.contain('Load reference from array');
res.body.result.tooltip.should.contain('Load reference from array');
res.body.result.url.should.contain('https://docs.oracle.com/javase/specs/');
})
.catch(err => { throw err; });
});
it('should respond to json for unknown opcodes', () => {
return chai.request(app)
.get('/asm/NOTANOPCODE')
.set('Accept', 'application/json')
.then(res => {
res.should.have.status(200);
res.should.be.json;
res.body.found.should.equal(false);
})
.catch(err => { throw err; });
});
});