Skip to content

Commit 0cab7df

Browse files
committed
--files -> --to
1 parent 5ba6041 commit 0cab7df

File tree

7 files changed

+142
-75
lines changed

7 files changed

+142
-75
lines changed

.polystat.conf

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
polystat {
2-
lang = eo
3-
input = sandbox
4-
outputTo = .
2+
lang = java
3+
input = sandbox_java
54
tempDir = tmp
65
outputFormats = [sarif]
6+
outputs = {
7+
console = true,
8+
dirs = [out],
9+
files = [./results/out.txt],
10+
}
711
}

README.md

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Prints all the available config keys. The configuration file format is [HOCON](h
1010
1111
Get the plain text console output from analyzing Java files. The Java files are in the directory `src/main/java`.
1212

13-
> `polystat eo --in tmp --sarif --files polystat_out`
13+
> `polystat eo --in tmp --sarif --to dir=polystat_out`
1414
1515
Write the [SARIF](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) JSON files to `polystat_out/sarif` from analysing the `tmp` directory with `.eo` files.
1616

@@ -21,8 +21,22 @@ Write the [SARIF](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.ht
2121
The description follows [this guide](https://en.wikipedia.org/wiki/Command-line_interface#Command_description_syntax).
2222
> Note: {a | b | c} means a set of _mutually-exclusive_ items.
2323
```
24-
polystat {eo | python} [--tmp <path>] [--in <path>] [{--include <rule...> | --exclude <rule...>}] [--sarif] [--files [path]]
25-
polystat java [--j2eo <path>] [--tmp <path>] [--in <path>] [{--include <rule...> | --exclude <rule...>}] [--sarif] [--files <path>]
24+
polystat
25+
{eo | python}
26+
[--tmp <path>]
27+
[--in <path>]
28+
[{--include <rule...> | --exclude <rule...>}]
29+
[--sarif]
30+
[--to { console | dir=<path>| file=<path> }]...
31+
polystat
32+
java
33+
[--j2eo-version <string>]
34+
[--j2eo <path>]
35+
[--tmp <path>]
36+
[--in <path>]
37+
[{--include <rule...> | --exclude <rule...>}]
38+
[--sarif]
39+
[--to { console | dir=<path>| file=<path> }]...
2640
polystat [--version] [--help] [--config <path>]
2741
polystat list [--config | -c]
2842
```
@@ -34,19 +48,24 @@ polystat list [--config | -c]
3448

3549
## Configuration options
3650
* `--include` and `--exclude` respectively define which rules should be included/excluded from the analysis run. These options are mutually exclusive, so specifying both should not be valid. If neither option is specified, all the available analyzers will be run. The list of available rule specifiers can be found via `polystat list` command.
37-
* `--j2eo` options allows users to specify the path to the j2eo executable jar. If it's not specified, it looks for one in the current working diretory.
51+
* `--j2eo` option allows users to specify the path to the j2eo executable jar. If it's not specified, it looks for one in the current working diretory.
3852
If it's not present in the current working directory, download one from Maven Central (for now, the version is hardcoded to be 0.4.0).
53+
* `--j2eo-version` option allows users to specify which version of `j2eo` should be downloaded.
3954

4055
## Output configuration
4156
* `--sarif` option means that the command will produce the output in the [SARIF](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) format in addition to output in other formats (if any).
42-
* `--files <path>` option specifies whether the output should be written to the output directory instead of writing it to the console.
43-
If the path is specified, the command will write the files to the given path. The path is assumed to be an empty directory. If it is not, its contents will be purged.
57+
* `--to { console | dir=<path>| file=<path> }` is a repeatable option that specifies where the output should be written. If this option is not specified, no output is produced.
58+
* `--to dir=<path>` means that the files will be written to the given path. The path is assumed to be an empty directory. If it is not, its contents will be purged.
4459
* If an additional output format is specified (e.g. `--sarif`), then the files created by the analyzer will be written in the respective subdirectory. For example, in case of `--sarif`, the SARIF files will be located in `path/sarif/`. The console output is not written anywhere. Therefore, if none of the output format options (e.g. `--sarif`) are specified, no files are produced.
4560
* The output format options (e.g. `--sarif`) also determine the extension of the output files. In case of `--sarif` the extension would be `.sarif.json`.
4661
* If `--in` option specifies a directory, the structure of the output directory will be similar to the structure of the input directory.
4762
* If `--in` specifies a single file, the file with the analysis output for this file will be written to the output directory.
4863
* If `--in` is not specified, the generated file will be called `stdin` + the relevant extension.
4964

65+
* `--to file=<path>` means that the results of analysis for all the files will be written to the file at the given path. For example, for `--sarif` output format this will a JSON array of `sarif-log` objects.
66+
67+
* `--to console` specifies whether the output should be written to console. The specification doesn't prevent the user from specifying multiple instances of this option. In this case, the output will be written to console as if just one instance of `--to console` was present. If it's not present the output is not written to console.
68+
5069
## `polystat list`
5170
* If `--config` or `-c` is specified, prints to console the description of all the possible configuration keys for the HOCON config file. If not, prints the specifiers for all the available analyzer rules.
5271

sandbox_java/MutualRec.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
class Base {
2-
int f(int x) {
3-
return x;
4-
}
51

6-
int g(int x) {
7-
return f(x);
2+
class MutualRec {
3+
class Base {
4+
int f(int x) {
5+
return x;
6+
}
7+
8+
int g(int x) {
9+
return f(x);
10+
}
811
}
9-
}
1012

11-
class Derived extends Base {
12-
@Override
13-
int f(int x) {
14-
return g(x);
13+
class Derived extends Base {
14+
@Override
15+
int f(int x) {
16+
return g(x);
17+
}
1518
}
1619
}

src/main/scala/org/polystat/EO.scala

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,17 @@ object EO:
2626
for
2727
_ <- IO.println(s"Analyzing $codePath...")
2828
analyzed <- runAnalyzers(cfg.filteredAnalyzers)(code)
29-
_ <- cfg.output match
30-
case Output.ToConsole => IO.println(analyzed)
31-
case Output.ToDirectory(out) =>
32-
cfg.fmts.traverse_ { case OutputFormat.Sarif =>
33-
val outPath =
34-
out / "sarif" / codePath.replaceExt(".sarif.json")
35-
val sarifJson = SarifOutput(analyzed).json.toString
36-
IO.println(s"Writing results to $outPath") *>
37-
writeOutputTo(outPath)(sarifJson)
38-
}
29+
_ <- if cfg.output.console then IO.println(analyzed) else IO.unit
30+
_ <- cfg.fmts.traverse_ { case OutputFormat.Sarif =>
31+
val sarifJson = SarifOutput(analyzed).json.toString
32+
cfg.output.dirs.traverse_(out =>
33+
val outPath =
34+
out / "sarif" / codePath.replaceExt(".sarif.json")
35+
36+
IO.println(s"Writing results to $outPath") *>
37+
writeOutputTo(outPath)(sarifJson)
38+
)
39+
}
3940
yield ()
4041
}
4142
.compile

src/main/scala/org/polystat/HoconConfig.scala

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,16 @@ case class HoconConfig(path: Path):
3939
case None => IO.pure(Input.FromStdin)
4040
}
4141
private val tmp = hocon(keys.tempDir).as[Path].option
42-
private val outputTo = hocon(keys.outputTo).as[Path].option.map {
43-
case Some(path) => Output.ToDirectory(path)
44-
case None => Output.ToConsole
42+
private val outputsDirs =
43+
hocon(keys.outputsDirs).as[List[Path]].default(List())
44+
private val outputsFiles =
45+
hocon(keys.outputsFiles).as[List[Path]].default(List())
46+
private val outputsConsole =
47+
hocon(keys.outputsConsole).as[Boolean].default(false)
48+
49+
private val outputs = (outputsDirs, outputsFiles, outputsConsole).parMapN {
50+
case (dirs, files, console) =>
51+
Output(dirs = dirs, files = files, console = console)
4552
}
4653
private val outputFormats =
4754
hocon(keys.outputFormats).as[List[OutputFormat]].default(List.empty)
@@ -53,8 +60,8 @@ case class HoconConfig(path: Path):
5360
.option
5461

5562
val config: ConfigValue[IO, PolystatUsage.Analyze] =
56-
(j2eo, inex, input, tmp, outputTo, outputFormats, lang).parMapN {
57-
case (j2eo, inex, input, tmp, outputTo, outputFormats, lang) =>
63+
(j2eo, inex, input, tmp, outputs, outputFormats, lang).parMapN {
64+
case (j2eo, inex, input, tmp, outputs, outputFormats, lang) =>
5865
PolystatUsage.Analyze(
5966
language = lang match
6067
case Java(_) => Java(j2eo)
@@ -65,7 +72,7 @@ case class HoconConfig(path: Path):
6572
input = input,
6673
tmp = tmp,
6774
outputFormats = outputFormats,
68-
output = outputTo,
75+
output = outputs,
6976
),
7077
)
7178
}
@@ -83,6 +90,9 @@ object HoconConfig:
8390
val includeRules = "includeRules"
8491
val excludeRules = "excludeRules"
8592
val j2eo = "j2eo"
93+
val outputsConsole = "outputs.console"
94+
val outputsDirs = "outputs.dirs"
95+
val outputsFiles = "output.files"
8696
val explanation = s"""
8797
|$toplevel.$inputLanguage
8898
| The type of input files which will be analyzed. This key must be present.
@@ -111,6 +121,13 @@ object HoconConfig:
111121
| If both are specified, $toplevel.$includeRules takes precedence.
112122
| The list of available rule specifiers can be found by running:
113123
| polystat.jar list
124+
|$toplevel.$outputsConsole
125+
| Produce console output?
126+
|$toplevel.$outputsDirs
127+
| A list of directories to write files to.
128+
|$toplevel.$outputsFiles
129+
| A list of files to write aggregated output to.
130+
|
114131
|""".stripMargin
115132
end keys
116133
extension (v: HoconConfigValue)
@@ -151,8 +168,16 @@ object HoconConfig:
151168
_.toNelString.map(Exclude(_))
152169
)
153170

154-
private given ConfigDecoder[HoconConfigValue, List[OutputFormat]] =
171+
private given hocon2listFormat
172+
: ConfigDecoder[HoconConfigValue, List[OutputFormat]] =
155173
ConfigDecoder[HoconConfigValue].mapOption(keys.outputFormats) {
156174
_.toListString.traverse(_.asOutputFormat)
157175
}
176+
177+
private given hocon2listPath: ConfigDecoder[HoconConfigValue, List[Path]] =
178+
ConfigDecoder[HoconConfigValue].mapOption("paths") {
179+
_.toListString.traverse(path =>
180+
ConfigDecoder[String, Path].decode(key = None, path).toOption
181+
)
182+
}
158183
end HoconConfig

src/main/scala/org/polystat/PolystatConfig.scala

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ object PolystatConfig:
1818
output: Output,
1919
)
2020

21-
case class ProcessedConfig(
21+
final case class ProcessedConfig(
2222
filteredAnalyzers: List[ASTAnalyzer[IO]],
2323
tempDir: IO[Path],
2424
input: Input,
@@ -51,9 +51,10 @@ object PolystatConfig:
5151
enum OutputFormat:
5252
case Sarif
5353

54-
enum Output:
55-
case ToDirectory(path: Path)
56-
case ToConsole
57-
end Output
54+
final case class Output(
55+
dirs: List[Path],
56+
files: List[Path],
57+
console: Boolean,
58+
)
5859

5960
end PolystatConfig

src/main/scala/org/polystat/PolystatOpts.scala

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,39 +9,17 @@ import com.monovore.decline.Opts
99
import fs2.io.file.Files
1010
import fs2.io.file.Path
1111
import org.polystat.PolystatConfig.*
12-
12+
import com.monovore.decline.Argument
1313
import java.io.FileNotFoundException
1414
import java.nio.file.Path as JPath
15+
import cats.data.ValidatedNel
1516

1617
import IncludeExclude.*
1718
import Validated.*
1819
import InputUtils.toInput
20+
import com.monovore.decline.Argument
1921

20-
object PolystatOpts extends IOApp.Simple:
21-
22-
override def run: IO[Unit] =
23-
val opts = Seq(
24-
"--config",
25-
"aboba",
26-
"--version",
27-
28-
// "c++",
29-
// "--in",
30-
// "tmp",
31-
// "--include",
32-
// "1",
33-
// "--include",
34-
// "2",
35-
// "--files",
36-
// "sandbox"
37-
)
38-
39-
polystat
40-
.parse(opts)
41-
.map(a => a.flatMap(IO.println))
42-
.leftMap(IO.println)
43-
.merge
44-
end run
22+
object PolystatOpts:
4523

4624
def polystat: Command[IO[PolystatUsage]] = Command(
4725
name = "polystat",
@@ -170,16 +148,52 @@ object PolystatOpts extends IOApp.Simple:
170148
sarif
171149
).sequence.map(_.flattenOption)
172150

173-
// TODO: this doesn't really conform to spec,
174-
// because decline doesn't support optional arguments to options
175-
// https://github.com/bkirwi/decline/issues/350
151+
private enum OutputArg:
152+
case File(path: Path)
153+
case Directory(path: Path)
154+
case Console
155+
end OutputArg
156+
157+
private given Argument[OutputArg] with
158+
def read(string: String): ValidatedNel[String, OutputArg] =
159+
val KVArg = "(.*)=(.*)".r
160+
string match
161+
case KVArg(key, value) =>
162+
key match
163+
case "dir" =>
164+
Argument[JPath]
165+
.read(value)
166+
.map(path => OutputArg.Directory(Path.fromNioPath(path)))
167+
case "file" =>
168+
Argument[JPath]
169+
.read(value)
170+
.map(path => OutputArg.File(Path.fromNioPath(path)))
171+
case other =>
172+
Validated.invalidNel(s"Unknown key in `--to` option: $other")
173+
case "console" => Validated.valid(OutputArg.Console)
174+
case other => Validated.invalidNel(s"Unknown argument: $string")
175+
end match
176+
end read
177+
def defaultMetavar: String = "console | dir=<path> | file=<path>"
178+
end given
179+
176180
def files: Opts[Output] = Opts
177-
.option[JPath](
178-
long = "files",
181+
.options[OutputArg](
182+
long = "to",
179183
help = "Create output files in the specified path",
180184
)
181-
.map(p => Output.ToDirectory(Path.fromNioPath(p)))
182-
.orElse(Output.ToConsole.pure[Opts])
185+
.orEmpty
186+
.map(args =>
187+
val initialState = Output(dirs = List(), files = List(), console = false)
188+
args.foldLeft(initialState) {
189+
case (acc, OutputArg.File(path)) =>
190+
acc.copy(files = acc.files.prepended(path))
191+
case (acc, OutputArg.Directory(path)) =>
192+
acc.copy(dirs = acc.dirs.prepended(path))
193+
case (acc, OutputArg.Console) =>
194+
if acc.console then acc else acc.copy(console = true)
195+
}
196+
)
183197

184198
def analyzerConfig: Opts[IO[AnalyzerConfig]] =
185199
(inex, in, tmp, outputFormats, files).mapN {

0 commit comments

Comments
 (0)