@appthreat/atom 2.1.13 → 2.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/package.json +3 -2
  2. package/plugins/bin/atom +1 -1
  3. package/plugins/bin/atom.bat +1 -1
  4. package/plugins/composer/installed.php +6 -6
  5. package/plugins/lib/io.appthreat.atom-2.1.14-classpath.jar +0 -0
  6. package/plugins/lib/{io.appthreat.atom-2.1.13.jar → io.appthreat.atom-2.1.14.jar} +0 -0
  7. package/plugins/lib/{io.appthreat.c2cpg_3-2.3.3.jar → io.appthreat.c2cpg_3-2.3.5.jar} +0 -0
  8. package/plugins/lib/{io.appthreat.dataflowengineoss_3-2.3.3.jar → io.appthreat.dataflowengineoss_3-2.3.5.jar} +0 -0
  9. package/plugins/lib/{io.appthreat.javasrc2cpg_3-2.3.3.jar → io.appthreat.javasrc2cpg_3-2.3.5.jar} +0 -0
  10. package/plugins/lib/{io.appthreat.jimple2cpg_3-2.3.3.jar → io.appthreat.jimple2cpg_3-2.3.5.jar} +0 -0
  11. package/plugins/lib/{io.appthreat.jssrc2cpg_3-2.3.3.jar → io.appthreat.jssrc2cpg_3-2.3.5.jar} +0 -0
  12. package/plugins/lib/{io.appthreat.php2atom_3-2.3.3.jar → io.appthreat.php2atom_3-2.3.5.jar} +0 -0
  13. package/plugins/lib/{io.appthreat.pysrc2cpg_3-2.3.3.jar → io.appthreat.pysrc2cpg_3-2.3.5.jar} +0 -0
  14. package/plugins/lib/{io.appthreat.ruby2atom_3-2.3.3.jar → io.appthreat.ruby2atom_3-2.3.5.jar} +0 -0
  15. package/plugins/lib/{io.appthreat.semanticcpg_3-2.3.3.jar → io.appthreat.semanticcpg_3-2.3.5.jar} +0 -0
  16. package/plugins/lib/{io.appthreat.x2cpg_3-2.3.3.jar → io.appthreat.x2cpg_3-2.3.5.jar} +0 -0
  17. package/plugins/lib/{org.slf4j.slf4j-api-2.0.16.jar → org.slf4j.slf4j-api-2.0.17.jar} +0 -0
  18. package/plugins/lib/org.slf4j.slf4j-nop-2.0.17.jar +0 -0
  19. package/plugins/rubyastgen/bundle/ruby/3.4.0/bundler/gems/ruby_ast_gen-d8b6ffa44317/ruby_ast_gen.gemspec +1 -1
  20. package/plugins/rubyastgen/bundle/ruby/3.4.0/extensions/x86_64-linux/3.4.0/racc-1.8.1/gem_make.out +5 -5
  21. package/scalasem.js +365 -0
  22. package/utils.mjs +27 -2
  23. package/plugins/lib/io.appthreat.atom-2.1.13-classpath.jar +0 -0
  24. package/plugins/lib/org.slf4j.slf4j-nop-2.0.16.jar +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appthreat/atom",
3
- "version": "2.1.13",
3
+ "version": "2.1.14",
4
4
  "description": "Create atom (⚛) representation for your application, packages and libraries",
5
5
  "exports": "./index.js",
6
6
  "type": "module",
@@ -20,7 +20,8 @@
20
20
  "atom": "index.js",
21
21
  "astgen": "astgen.js",
22
22
  "phpastgen": "phpastgen.js",
23
- "rbastgen": "rbastgen.js"
23
+ "rbastgen": "rbastgen.js",
24
+ "scalasem": "scalasem.js"
24
25
  },
25
26
  "engines": {
26
27
  "node": ">=16.0.0"
package/plugins/bin/atom CHANGED
@@ -360,7 +360,7 @@ declare -r lib_dir="$(realpath "${app_home}/../lib")"
360
360
  declare -a app_mainclass=('io.appthreat.atom.Atom')
361
361
 
362
362
  declare -r script_conf_file="${app_home}/../conf/application.ini"
363
- declare -r app_classpath="$lib_dir/io.appthreat.atom-2.1.13-classpath.jar"
363
+ declare -r app_classpath="$lib_dir/io.appthreat.atom-2.1.14-classpath.jar"
364
364
 
365
365
  # java_cmd is overrode in process_args when -java-home is used
366
366
  declare java_cmd=$(get_java_cmd)
@@ -40,7 +40,7 @@ rem "-J" is stripped, "-D" is left as is, and everything is appended to JAVA_OPT
40
40
  set _JAVA_PARAMS=
41
41
  set _APP_ARGS=
42
42
 
43
- set "APP_CLASSPATH=%APP_LIB_DIR%\io.appthreat.atom-2.1.13-classpath.jar"
43
+ set "APP_CLASSPATH=%APP_LIB_DIR%\io.appthreat.atom-2.1.14-classpath.jar"
44
44
  set "APP_MAIN_CLASS=io.appthreat.atom.Atom"
45
45
  set "SCRIPT_CONF_FILE=%APP_HOME%\conf\application.ini"
46
46
 
@@ -1,9 +1,9 @@
1
1
  <?php return array(
2
2
  'root' => array(
3
3
  'name' => '__root__',
4
- 'pretty_version' => 'v2.1.13',
5
- 'version' => '2.1.13.0',
6
- 'reference' => 'cbf0caab75d535ac02719b3c97e9978955da5721',
4
+ 'pretty_version' => 'v2.1.14',
5
+ 'version' => '2.1.14.0',
6
+ 'reference' => 'd1252e282dc3ddc3b51914f8e21fe48fd40d01ba',
7
7
  'type' => 'library',
8
8
  'install_path' => __DIR__ . '/../../',
9
9
  'aliases' => array(),
@@ -11,9 +11,9 @@
11
11
  ),
12
12
  'versions' => array(
13
13
  '__root__' => array(
14
- 'pretty_version' => 'v2.1.13',
15
- 'version' => '2.1.13.0',
16
- 'reference' => 'cbf0caab75d535ac02719b3c97e9978955da5721',
14
+ 'pretty_version' => 'v2.1.14',
15
+ 'version' => '2.1.14.0',
16
+ 'reference' => 'd1252e282dc3ddc3b51914f8e21fe48fd40d01ba',
17
17
  'type' => 'library',
18
18
  'install_path' => __DIR__ . '/../../',
19
19
  'aliases' => array(),
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
10
10
  s.require_paths = ["lib".freeze]
11
11
  s.authors = ["David Baker Effendi".freeze, "Andrei Dreyer".freeze, "Team AppThreat".freeze]
12
12
  s.bindir = "exe".freeze
13
- s.date = "2025-03-05"
13
+ s.date = "2025-03-10"
14
14
  s.description = "A Ruby parser than dumps the AST as JSON output for atom `rubysrc2cpg` frontend".freeze
15
15
  s.email = ["dave@whirlylabs.com".freeze, "andrei@whirlylabs.com".freeze, "hello@appthreat.com".freeze]
16
16
  s.executables = ["ruby_ast_gen".freeze]
@@ -3,16 +3,16 @@ current directory: /home/runner/work/atom/atom/wrapper/nodejs/plugins/rubyastgen
3
3
  creating Makefile
4
4
 
5
5
  current directory: /home/runner/work/atom/atom/wrapper/nodejs/plugins/rubyastgen/bundle/ruby/3.4.0/gems/racc-1.8.1/ext/racc/cparse
6
- make DESTDIR\= sitearchdir\=./.gem.20250305-2774-qrpo6g sitelibdir\=./.gem.20250305-2774-qrpo6g clean
6
+ make DESTDIR\= sitearchdir\=./.gem.20250310-2778-ho21ul sitelibdir\=./.gem.20250310-2778-ho21ul clean
7
7
 
8
8
  current directory: /home/runner/work/atom/atom/wrapper/nodejs/plugins/rubyastgen/bundle/ruby/3.4.0/gems/racc-1.8.1/ext/racc/cparse
9
- make DESTDIR\= sitearchdir\=./.gem.20250305-2774-qrpo6g sitelibdir\=./.gem.20250305-2774-qrpo6g
9
+ make DESTDIR\= sitearchdir\=./.gem.20250310-2778-ho21ul sitelibdir\=./.gem.20250310-2778-ho21ul
10
10
  compiling cparse.c
11
11
  linking shared-object racc/cparse.so
12
12
 
13
13
  current directory: /home/runner/work/atom/atom/wrapper/nodejs/plugins/rubyastgen/bundle/ruby/3.4.0/gems/racc-1.8.1/ext/racc/cparse
14
- make DESTDIR\= sitearchdir\=./.gem.20250305-2774-qrpo6g sitelibdir\=./.gem.20250305-2774-qrpo6g install
15
- /usr/bin/install -c -m 0755 cparse.so ./.gem.20250305-2774-qrpo6g/racc
14
+ make DESTDIR\= sitearchdir\=./.gem.20250310-2778-ho21ul sitelibdir\=./.gem.20250310-2778-ho21ul install
15
+ /usr/bin/install -c -m 0755 cparse.so ./.gem.20250310-2778-ho21ul/racc
16
16
 
17
17
  current directory: /home/runner/work/atom/atom/wrapper/nodejs/plugins/rubyastgen/bundle/ruby/3.4.0/gems/racc-1.8.1/ext/racc/cparse
18
- make DESTDIR\= sitearchdir\=./.gem.20250305-2774-qrpo6g sitelibdir\=./.gem.20250305-2774-qrpo6g clean
18
+ make DESTDIR\= sitearchdir\=./.gem.20250310-2778-ho21ul sitelibdir\=./.gem.20250310-2778-ho21ul clean
package/scalasem.js ADDED
@@ -0,0 +1,365 @@
1
+ #!/usr/bin/env node
2
+ // Usage: scalasem $(pwd) slices.json
3
+ import { tmpdir } from "node:os";
4
+ import { basename, dirname, join, relative } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { detectScala, detectScalac, getAllFiles } from "./utils.mjs";
7
+ import process from "node:process";
8
+ import {
9
+ existsSync,
10
+ mkdtempSync,
11
+ mkdirSync,
12
+ readFileSync,
13
+ rmSync,
14
+ writeFileSync
15
+ } from "node:fs";
16
+
17
+ function main(argvs) {
18
+ if (!detectScala() && !detectScalac()) {
19
+ console.warn("Scala is not installed!");
20
+ return false;
21
+ }
22
+ let configFiles = getAllFiles(argvs[0], "routes");
23
+ configFiles = configFiles.concat(getAllFiles(argvs[0], ".conf"));
24
+ let tastyFiles = getAllFiles(argvs[0], ".tasty");
25
+ if (!tastyFiles.length) {
26
+ let buildTool = "sbt";
27
+ const millFiles = getAllFiles(argvs[0], "build.mill");
28
+ if (millFiles.length) {
29
+ buildTool = "mill";
30
+ }
31
+ const cwd = process.env.ATOM_CWD || process.cwd();
32
+ let compileCommand =
33
+ process?.env[`${buildTool.toUpperCase()}_COMPILE_COMMAND`] || "compile";
34
+ if (process.env.SCALA_VERSION && buildTool === "sbt") {
35
+ compileCommand = `++${process.env.SCALA_VERSION} ${compileCommand}`;
36
+ } else {
37
+ // Detect crossScalaVersions
38
+ const scalaVersion = findScalaVersion(cwd);
39
+ if (scalaVersion) {
40
+ compileCommand = `++${scalaVersion} ${compileCommand}`;
41
+ }
42
+ }
43
+ console.log(`Executing '${buildTool} ${compileCommand}' in ${argvs[0]}`);
44
+ const result = spawnSync(buildTool, compileCommand.split(" "), {
45
+ encoding: "utf-8",
46
+ cwd,
47
+ stdio: "ignore",
48
+ stderr: "inherit",
49
+ env: process.env,
50
+ timeout: process.env.ATOM_TIMEOUT || process.env.ASTGEN_TIMEOUT
51
+ });
52
+ if (result.error || result.status !== 0) {
53
+ if (result.stderr) {
54
+ console.log(result.stderr);
55
+ }
56
+ return false;
57
+ }
58
+ tastyFiles = getAllFiles(argvs[0], ".tasty");
59
+ console.log(`Obtained ${tastyFiles.length} IR files after compilation.`);
60
+ }
61
+ const slicesFile =
62
+ argvs.length > 1 ? argvs[1] : join(argvs[0], "slices.json");
63
+ createSemanticSlices(tastyFiles, configFiles, slicesFile);
64
+ }
65
+ main(process.argv.slice(2));
66
+
67
+ function findScalaVersion(cwd) {
68
+ let scalaVersion;
69
+ const buildSbtFile = join(cwd, "build.sbt");
70
+ if (existsSync(buildSbtFile)) {
71
+ const buildData = readFileSync(buildSbtFile, "utf-8");
72
+ for (let line of buildData.split("\n")) {
73
+ if (line.trim().includes("val ") && line.includes("scala")) {
74
+ const match = line.match(/"(3\.[^"]+)"/);
75
+ if (match) {
76
+ return match[1];
77
+ }
78
+ }
79
+ if (line.trim().includes("crossScalaVersions")) {
80
+ const crossVersions = line.split("crossScalaVersions").pop().trim();
81
+ if (crossVersions.includes("3.")) {
82
+ const match = crossVersions.match(/"(3\.[^"]+)"/);
83
+ if (match) {
84
+ return match[1];
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ return scalaVersion;
91
+ }
92
+
93
+ function createSemanticSlices(tastyFiles, configFiles, slicesFile) {
94
+ const outDir = mkdtempSync(join(tmpdir(), "scalasem-"));
95
+ const MAX_BUFFER =
96
+ Number.parseInt(process.env.ATOM_MAX_BUFFER) || 100 * 1024 * 1024;
97
+ const cwd = process.env.ATOM_CWD || process.cwd();
98
+ const slices = {};
99
+ slices.config = parseConfigFiles(configFiles);
100
+ for (const af of tastyFiles) {
101
+ const result = spawnSync(
102
+ process.env.SCALAC_CMD || "scalac",
103
+ ["-color:never", "-print-tasty", af],
104
+ {
105
+ encoding: "utf-8",
106
+ cwd,
107
+ env: process.env,
108
+ maxBuffer: MAX_BUFFER,
109
+ timeout: process.env.ATOM_TIMEOUT || process.env.ASTGEN_TIMEOUT
110
+ }
111
+ );
112
+ if (result.error || result.status !== 0) {
113
+ if (result.stderr) {
114
+ console.log(result.stderr);
115
+ }
116
+ }
117
+ if (result.stdout) {
118
+ let fileOutDir = join(outDir, relative(cwd, dirname(af)));
119
+ const scalaDir = relative(cwd, dirname(af)).replace(
120
+ new RegExp("target/scala-(.)*/classes"),
121
+ ""
122
+ );
123
+ if (fileOutDir.includes("classes")) {
124
+ fileOutDir = fileOutDir.replace(
125
+ new RegExp("target/scala-(.)*/classes"),
126
+ ""
127
+ );
128
+ }
129
+ mkdirSync(fileOutDir, { recursive: true });
130
+ const astFile = join(
131
+ fileOutDir,
132
+ basename(af).replace(".tasty", ".scala.ast")
133
+ );
134
+ const scalaFile = join(
135
+ scalaDir,
136
+ basename(af).replace(".tasty", ".scala")
137
+ );
138
+ writeFileSync(astFile, Buffer.from(result.stdout).toString());
139
+ const usages = parseTasty(astFile);
140
+ slices[usages.sourceFile || scalaFile] = usages;
141
+ rmSync(astFile);
142
+ }
143
+ }
144
+ const slicesJson = JSON.stringify(slices, null, null);
145
+ writeFileSync(slicesFile, slicesJson);
146
+ if (!Object.keys(slices).length) {
147
+ console.log("Empty slices file created.");
148
+ } else {
149
+ console.log(
150
+ `Slices file ${slicesFile} created successfully with ${
151
+ Object.keys(slices).length
152
+ } entries.`
153
+ );
154
+ }
155
+ if (outDir?.startsWith(tmpdir())) {
156
+ rmSync(outDir, { recursive: true });
157
+ }
158
+ }
159
+
160
+ function parseTasty(tastyAstFile) {
161
+ const astData = readFileSync(tastyAstFile, "utf-8");
162
+ let namesMode = false;
163
+ let treesMode = false;
164
+ let sourcePathsMode = false;
165
+ const literals = new Set();
166
+ const usedTypes = new Set();
167
+ const tags = new Set();
168
+ let sourceFile;
169
+ for (let line of astData.split("\n")) {
170
+ line = line.replace("\r", "").trim();
171
+ if (!line.length || line.startsWith("---")) {
172
+ continue;
173
+ }
174
+ if (line.startsWith("Names ") || line.startsWith("Names:")) {
175
+ namesMode = true;
176
+ }
177
+ if (line.startsWith("Trees ") || line.startsWith("Trees:")) {
178
+ namesMode = false;
179
+ treesMode = true;
180
+ }
181
+ if (line.startsWith("Positions ") || line.startsWith("positions:")) {
182
+ namesMode = false;
183
+ treesMode = false;
184
+ }
185
+ if (namesMode) {
186
+ // 3: api
187
+ if (line.includes(": ")) {
188
+ const literal = line.split(": ").pop().trim();
189
+ if (literal.length > 1) {
190
+ literals.add(literal);
191
+ }
192
+ }
193
+ }
194
+ if (treesMode && line.includes(" Signature(")) {
195
+ // 139: SELECTin(12) 38 [<init>[Signed Signature(List(play.api.mvc.MessagesControllerComponents),play.api.mvc.MessagesAbstractController) @<init>]]
196
+ const signatureTypes = line
197
+ .split(" Signature(")
198
+ .pop()
199
+ .split(") ")[0]
200
+ .replaceAll("List(", "")
201
+ .replaceAll(")", "")
202
+ .split(",");
203
+ for (let sig of signatureTypes) {
204
+ sig = sig.trim();
205
+ if (
206
+ sig.length > 3 &&
207
+ !sig.startsWith("scala.") &&
208
+ !sig.startsWith("java.") &&
209
+ !sig.startsWith("javax.inject.")
210
+ ) {
211
+ usedTypes.add(sig);
212
+ if (sig.startsWith("play.api.")) {
213
+ tags.add("framework");
214
+ }
215
+ if (
216
+ sig.startsWith("play.api.data.Form") ||
217
+ sig.startsWith("play.api.mvc.Request") ||
218
+ sig.startsWith("play.twirl.api")
219
+ ) {
220
+ tags.add("framework-input");
221
+ }
222
+ if (
223
+ sig.startsWith("play.twirl.api.Html") ||
224
+ sig.startsWith("play.api.mvc.Result") ||
225
+ sig.startsWith("play.api.mvc.Action")
226
+ ) {
227
+ tags.add("framework-output");
228
+ }
229
+ if (
230
+ sig.startsWith("play.api.routing.") ||
231
+ sig.startsWith("play.core.routing") ||
232
+ sig.startsWith("router.RoutesPrefix")
233
+ ) {
234
+ tags.add("framework-route");
235
+ }
236
+ if (
237
+ sig.startsWith("slick.sql.") ||
238
+ sig.startsWith("play.db.") ||
239
+ sig.startsWith("slick.jdbc.")
240
+ ) {
241
+ tags.add("database");
242
+ }
243
+ }
244
+ }
245
+ }
246
+ if (line.includes("source paths:")) {
247
+ sourcePathsMode = true;
248
+ }
249
+ if (sourcePathsMode) {
250
+ if (line.includes(" [") && line.endsWith("]")) {
251
+ sourceFile = line.split(" [").pop().replace(/]/g, "");
252
+ sourcePathsMode = false;
253
+ } else if (line.includes(".scala") && line.includes(": ")) {
254
+ sourceFile = line.split(": ").pop().trim();
255
+ sourcePathsMode = false;
256
+ }
257
+ }
258
+ if (!namesMode && !treesMode && !sourcePathsMode) {
259
+ continue;
260
+ }
261
+ }
262
+ if (sourceFile?.includes("target")) {
263
+ tags.add("generated");
264
+ }
265
+ return {
266
+ sourceFile,
267
+ tags: Array.from(tags).sort(),
268
+ usedTypes: Array.from(usedTypes).sort(),
269
+ literals: Array.from(literals)
270
+ };
271
+ }
272
+
273
+ function parseConfigFiles(configFiles) {
274
+ const configMetadata = { routes: [] };
275
+ for (const aconfig of configFiles) {
276
+ if (aconfig.endsWith("routes")) {
277
+ const routes = parseRoutes(aconfig);
278
+ if (routes?.length) {
279
+ for (const aroute of routes) {
280
+ let duplicate = false;
281
+ for (const exisRoute of configMetadata.routes) {
282
+ if (
283
+ exisRoute.method === aroute.method &&
284
+ exisRoute.pattern === aroute.pattern
285
+ ) {
286
+ if (
287
+ exisRoute.controllerMethod &&
288
+ exisRoute.controllerMethod === aroute.controllerMethod
289
+ ) {
290
+ duplicate = true;
291
+ continue;
292
+ }
293
+ }
294
+ }
295
+ if (!duplicate) {
296
+ configMetadata["routes"].push(aroute);
297
+ }
298
+ }
299
+ }
300
+ }
301
+ }
302
+ if (configMetadata.routes.length) {
303
+ console.log("Found", configMetadata.routes.length, "routes.");
304
+ }
305
+ return configMetadata;
306
+ }
307
+
308
+ function parseRoutes(routesFile) {
309
+ const routes = [];
310
+ const routesData = readFileSync(routesFile, "utf-8");
311
+ for (let aline of routesData.split("\n")) {
312
+ aline = aline.replace("\r", "").trim();
313
+ if (aline.startsWith("#") || aline.startsWith("+")) {
314
+ continue;
315
+ }
316
+ const tmpA = aline.split(/\s+/);
317
+ if (tmpA.length < 2) {
318
+ continue;
319
+ }
320
+ // Ignore static assets
321
+ if (["/webjars"].includes(tmpA[1])) {
322
+ continue;
323
+ }
324
+ if (
325
+ [
326
+ "GET",
327
+ "PATCH",
328
+ "POST",
329
+ "OPTIONS",
330
+ "HEAD",
331
+ "DELETE",
332
+ "PUT",
333
+ "->"
334
+ ].includes(tmpA[0].toUpperCase())
335
+ ) {
336
+ let controllerMethod = tmpA.length > 2 ? tmpA[2] : undefined;
337
+ if (controllerMethod.includes("(")) {
338
+ controllerMethod = controllerMethod.split("(")[0];
339
+ }
340
+ // Exclude webjars
341
+ if (controllerMethod.startsWith("webjars.")) {
342
+ continue;
343
+ }
344
+ // Handle wildcards
345
+ if (tmpA[0] === "->") {
346
+ // We now need to parse a method called "routes" in the controllerMethod to identify the list of http methods
347
+ // Let's keep things simple for now
348
+ for (const m of ["GET", "PATCH", "POST", "DELETE", "PUT"]) {
349
+ routes.push({
350
+ method: m,
351
+ pattern: tmpA[1],
352
+ controllerMethod
353
+ });
354
+ }
355
+ } else {
356
+ routes.push({
357
+ method: tmpA[0],
358
+ pattern: tmpA[1],
359
+ controllerMethod
360
+ });
361
+ }
362
+ }
363
+ }
364
+ return routes;
365
+ }
package/utils.mjs CHANGED
@@ -28,7 +28,7 @@ const IGNORE_DIRS = process.env.ASTGEN_IGNORE_DIRS
28
28
 
29
29
  const IGNORE_FILE_PATTERN = new RegExp(
30
30
  process.env.ASTGEN_IGNORE_FILE_PATTERN ||
31
- "(conf|config|test|spec|min|three|\\.d)\\.(js|ts|jsx|tsx)$",
31
+ "(test|spec|min|three|\\.d)\\.(js|ts|jsx|tsx)$",
32
32
  "i"
33
33
  );
34
34
 
@@ -69,7 +69,12 @@ export const getAllFiles = (dir, extn, files, result, regex) => {
69
69
  // ignore
70
70
  }
71
71
  } else {
72
- if (regex.test(fileWithDir)) {
72
+ if (
73
+ regex.test(fileWithDir) ||
74
+ (extn &&
75
+ !extn.includes(".") &&
76
+ fileWithDir.toLowerCase().endsWith(extn.toLowerCase()))
77
+ ) {
73
78
  result.push(fileWithDir);
74
79
  }
75
80
  }
@@ -112,3 +117,23 @@ export const detectRuby = (versionNeeded) => {
112
117
  }
113
118
  return true;
114
119
  };
120
+
121
+ export const detectScala = () => {
122
+ let result = spawnSync(process.env.SCALA_CMD || "scala", ["--version"], {
123
+ encoding: "utf-8"
124
+ });
125
+ if (result.status !== 0 || result.error) {
126
+ return false;
127
+ }
128
+ return true;
129
+ };
130
+
131
+ export const detectScalac = () => {
132
+ let result = spawnSync(process.env.SCALAC_CMD || "scalac", ["--version"], {
133
+ encoding: "utf-8"
134
+ });
135
+ if (result.status !== 0 || result.error) {
136
+ return false;
137
+ }
138
+ return true;
139
+ };