@appthreat/atom
Version:
Create atom (⚛) representation for your application, packages and libraries
366 lines (359 loc) • 11 kB
JavaScript
// Usage: scalasem $(pwd) slices.json
import { tmpdir } from "node:os";
import { basename, dirname, join, relative } from "node:path";
import { spawnSync } from "node:child_process";
import { detectScala, detectScalac, getAllFiles } from "@appthreat/atom-common";
import process from "node:process";
import {
existsSync,
mkdtempSync,
mkdirSync,
readFileSync,
rmSync,
writeFileSync
} from "node:fs";
function main(argvs) {
if (!detectScala() && !detectScalac()) {
console.warn("Scala is not installed!");
return false;
}
let configFiles = getAllFiles(argvs[0], "routes");
configFiles = configFiles.concat(getAllFiles(argvs[0], ".conf"));
let tastyFiles = getAllFiles(argvs[0], ".tasty");
if (!tastyFiles.length) {
let buildTool = "sbt";
const millFiles = getAllFiles(argvs[0], "build.mill");
if (millFiles.length) {
buildTool = "mill";
}
const cwd = process.env.ATOM_CWD || process.cwd();
let compileCommand =
process?.env[`${buildTool.toUpperCase()}_COMPILE_COMMAND`] || "compile";
if (process.env.SCALA_VERSION && buildTool === "sbt") {
compileCommand = `++${process.env.SCALA_VERSION} ${compileCommand}`;
} else {
// Detect crossScalaVersions
const scalaVersion = findScalaVersion(cwd);
if (scalaVersion) {
compileCommand = `++${scalaVersion} ${compileCommand}`;
}
}
console.log(`Executing '${buildTool} ${compileCommand}' in ${argvs[0]}`);
const result = spawnSync(buildTool, compileCommand.split(" "), {
encoding: "utf-8",
cwd,
stdio: "ignore",
stderr: "inherit",
env: process.env,
timeout: process.env.ATOM_TIMEOUT || process.env.ASTGEN_TIMEOUT
});
if (result.error || result.status !== 0) {
if (result.stderr) {
console.log(result.stderr);
}
return false;
}
tastyFiles = getAllFiles(argvs[0], ".tasty");
console.log(`Obtained ${tastyFiles.length} IR files after compilation.`);
}
const slicesFile =
argvs.length > 1 ? argvs[1] : join(argvs[0], "slices.json");
createSemanticSlices(tastyFiles, configFiles, slicesFile);
}
main(process.argv.slice(2));
function findScalaVersion(cwd) {
let scalaVersion;
const buildSbtFile = join(cwd, "build.sbt");
if (existsSync(buildSbtFile)) {
const buildData = readFileSync(buildSbtFile, "utf-8");
for (let line of buildData.split("\n")) {
if (line.trim().includes("val ") && line.includes("scala")) {
const match = line.match(/"(3\.[^"]+)"/);
if (match) {
return match[1];
}
}
if (line.trim().includes("crossScalaVersions")) {
const crossVersions = line.split("crossScalaVersions").pop().trim();
if (crossVersions.includes("3.")) {
const match = crossVersions.match(/"(3\.[^"]+)"/);
if (match) {
return match[1];
}
}
}
}
}
return scalaVersion;
}
function createSemanticSlices(tastyFiles, configFiles, slicesFile) {
const outDir = mkdtempSync(join(tmpdir(), "scalasem-"));
const MAX_BUFFER =
Number.parseInt(process.env.ATOM_MAX_BUFFER) || 100 * 1024 * 1024;
const cwd = process.env.ATOM_CWD || process.cwd();
const slices = {};
slices.config = parseConfigFiles(configFiles);
for (const af of tastyFiles) {
const result = spawnSync(
process.env.SCALAC_CMD || "scalac",
["-color:never", "-print-tasty", af],
{
encoding: "utf-8",
cwd,
env: process.env,
maxBuffer: MAX_BUFFER,
timeout: process.env.ATOM_TIMEOUT || process.env.ASTGEN_TIMEOUT
}
);
if (result.error || result.status !== 0) {
if (result.stderr) {
console.log(result.stderr);
}
}
if (result.stdout) {
let fileOutDir = join(outDir, relative(cwd, dirname(af)));
const scalaDir = relative(cwd, dirname(af)).replace(
new RegExp("target/scala-(.)*/classes"),
""
);
if (fileOutDir.includes("classes")) {
fileOutDir = fileOutDir.replace(
new RegExp("target/scala-(.)*/classes"),
""
);
}
mkdirSync(fileOutDir, { recursive: true });
const astFile = join(
fileOutDir,
basename(af).replace(".tasty", ".scala.ast")
);
const scalaFile = join(
scalaDir,
basename(af).replace(".tasty", ".scala")
);
writeFileSync(astFile, Buffer.from(result.stdout).toString());
const usages = parseTasty(astFile);
slices[usages.sourceFile || scalaFile] = usages;
rmSync(astFile);
}
}
const slicesJson = JSON.stringify(slices, null, null);
writeFileSync(slicesFile, slicesJson);
if (!Object.keys(slices).length) {
console.log("Empty slices file created.");
} else {
console.log(
`Slices file ${slicesFile} created successfully with ${
Object.keys(slices).length
} entries.`
);
}
if (outDir?.startsWith(tmpdir())) {
rmSync(outDir, { recursive: true });
}
}
function parseTasty(tastyAstFile) {
const astData = readFileSync(tastyAstFile, "utf-8");
let namesMode = false;
let treesMode = false;
let sourcePathsMode = false;
const literals = new Set();
const usedTypes = new Set();
const tags = new Set();
let sourceFile;
for (let line of astData.split("\n")) {
line = line.replace("\r", "").trim();
if (!line.length || line.startsWith("---")) {
continue;
}
if (line.startsWith("Names ") || line.startsWith("Names:")) {
namesMode = true;
}
if (line.startsWith("Trees ") || line.startsWith("Trees:")) {
namesMode = false;
treesMode = true;
}
if (line.startsWith("Positions ") || line.startsWith("positions:")) {
namesMode = false;
treesMode = false;
}
if (namesMode) {
// 3: api
if (line.includes(": ")) {
const literal = line.split(": ").pop().trim();
if (literal.length > 1) {
literals.add(literal);
}
}
}
if (treesMode && line.includes(" Signature(")) {
// 139: SELECTin(12) 38 [<init>[Signed Signature(List(play.api.mvc.MessagesControllerComponents),play.api.mvc.MessagesAbstractController) @<init>]]
const signatureTypes = line
.split(" Signature(")
.pop()
.split(") ")[0]
.replaceAll("List(", "")
.replaceAll(")", "")
.split(",");
for (let sig of signatureTypes) {
sig = sig.trim();
if (
sig.length > 3 &&
!sig.startsWith("scala.") &&
!sig.startsWith("java.") &&
!sig.startsWith("javax.inject.")
) {
usedTypes.add(sig);
if (sig.startsWith("play.api.")) {
tags.add("framework");
}
if (
sig.startsWith("play.api.data.Form") ||
sig.startsWith("play.api.mvc.Request") ||
sig.startsWith("play.twirl.api")
) {
tags.add("framework-input");
}
if (
sig.startsWith("play.twirl.api.Html") ||
sig.startsWith("play.api.mvc.Result") ||
sig.startsWith("play.api.mvc.Action")
) {
tags.add("framework-output");
}
if (
sig.startsWith("play.api.routing.") ||
sig.startsWith("play.core.routing") ||
sig.startsWith("router.RoutesPrefix")
) {
tags.add("framework-route");
}
if (
sig.startsWith("slick.sql.") ||
sig.startsWith("play.db.") ||
sig.startsWith("slick.jdbc.")
) {
tags.add("database");
}
}
}
}
if (line.includes("source paths:")) {
sourcePathsMode = true;
}
if (sourcePathsMode) {
if (line.includes(" [") && line.endsWith("]")) {
sourceFile = line.split(" [").pop().replace(/]/g, "");
sourcePathsMode = false;
} else if (line.includes(".scala") && line.includes(": ")) {
sourceFile = line.split(": ").pop().trim();
sourcePathsMode = false;
}
}
if (!namesMode && !treesMode && !sourcePathsMode) {
continue;
}
}
if (sourceFile?.includes("target")) {
tags.add("generated");
}
return {
sourceFile,
tags: Array.from(tags).sort(),
usedTypes: Array.from(usedTypes).sort(),
literals: Array.from(literals)
};
}
function parseConfigFiles(configFiles) {
const configMetadata = { routes: [] };
for (const aconfig of configFiles) {
if (aconfig.endsWith("routes")) {
const routes = parseRoutes(aconfig);
if (routes?.length) {
for (const aroute of routes) {
let duplicate = false;
for (const exisRoute of configMetadata.routes) {
if (
exisRoute.method === aroute.method &&
exisRoute.pattern === aroute.pattern
) {
if (
exisRoute.controllerMethod &&
exisRoute.controllerMethod === aroute.controllerMethod
) {
duplicate = true;
continue;
}
}
}
if (!duplicate) {
configMetadata["routes"].push(aroute);
}
}
}
}
}
if (configMetadata.routes.length) {
console.log("Found", configMetadata.routes.length, "routes.");
}
return configMetadata;
}
function parseRoutes(routesFile) {
const routes = [];
const routesData = readFileSync(routesFile, "utf-8");
for (let aline of routesData.split("\n")) {
aline = aline.replace("\r", "").trim();
if (aline.startsWith("#") || aline.startsWith("+")) {
continue;
}
const tmpA = aline.split(/\s+/);
if (tmpA.length < 2) {
continue;
}
// Ignore static assets
if (["/webjars"].includes(tmpA[1])) {
continue;
}
if (
[
"GET",
"PATCH",
"POST",
"OPTIONS",
"HEAD",
"DELETE",
"PUT",
"->"
].includes(tmpA[0].toUpperCase())
) {
let controllerMethod = tmpA.length > 2 ? tmpA[2] : undefined;
if (controllerMethod.includes("(")) {
controllerMethod = controllerMethod.split("(")[0];
}
// Exclude webjars
if (controllerMethod.startsWith("webjars.")) {
continue;
}
// Handle wildcards
if (tmpA[0] === "->") {
// We now need to parse a method called "routes" in the controllerMethod to identify the list of http methods
// Let's keep things simple for now
for (const m of ["GET", "PATCH", "POST", "DELETE", "PUT"]) {
routes.push({
method: m,
pattern: tmpA[1],
controllerMethod
});
}
} else {
routes.push({
method: tmpA[0],
pattern: tmpA[1],
controllerMethod
});
}
}
}
return routes;
}