///|
/// Top-level Moon Doctor command.
pub(all) enum DoctorCommand {
Check
Rules
Explain(String)
} derive(Debug, Eq)
///|
/// Request parsed from both modern subcommands and legacy check syntax.
pub(all) struct CommandRequest {
command : DoctorCommand
target : String
format : OutputFormat
strict : Bool
help : Bool
config_path : String?
profile : String?
} derive(Debug, Eq)
///|
pub fn CommandRequest::default() -> CommandRequest {
{
command: Check,
target: ".",
format: Text,
strict: false,
help: false,
config_path: None,
profile: None,
}
}
///|
fn command_format(value : String) -> OutputFormat? {
match value {
"text" => Some(Text)
"json" => Some(Json)
"sarif" => Some(Sarif)
_ => None
}
}
///|
fn replace_request(
current : CommandRequest,
command? : DoctorCommand,
target? : String,
format? : OutputFormat,
strict? : Bool,
help? : Bool,
config_path? : String?,
profile? : String?,
) -> CommandRequest {
let next_command = match command {
Some(value) => value
None => current.command
}
let next_target = match target {
Some(value) => value
None => current.target
}
let next_format = match format {
Some(value) => value
None => current.format
}
let next_strict = match strict {
Some(value) => value
None => current.strict
}
let next_help = match help {
Some(value) => value
None => current.help
}
let next_config_path = match config_path {
Some(value) => value
None => current.config_path
}
let next_profile = match profile {
Some(value) => value
None => current.profile
}
{
command: next_command,
target: next_target,
format: next_format,
strict: next_strict,
help: next_help,
config_path: next_config_path,
profile: next_profile,
}
}
///|
fn parse_command_at(
args : Array[String],
index : Int,
request : CommandRequest,
command_seen : Bool,
) -> Result[CommandRequest, String] {
if index >= args.length() {
return Ok(request)
}
let token = args[index]
match token {
"--help" | "-h" =>
parse_command_at(
args,
index + 1,
replace_request(request, help=true),
command_seen,
)
"--strict" =>
parse_command_at(
args,
index + 1,
replace_request(request, strict=true),
command_seen,
)
"--format" =>
if index + 1 >= args.length() {
Err("--format requires text, json, or sarif")
} else {
match command_format(args[index + 1]) {
Some(format) =>
parse_command_at(
args,
index + 2,
replace_request(request, format~),
command_seen,
)
None => Err("unsupported format: " + args[index + 1])
}
}
"--config" =>
if index + 1 >= args.length() {
Err("--config requires a path")
} else {
parse_command_at(
args,
index + 2,
replace_request(request, config_path=Some(args[index + 1])),
command_seen,
)
}
"--profile" =>
if index + 1 >= args.length() {
Err("--profile requires default, github, mooncakes, or release")
} else {
parse_command_at(
args,
index + 2,
replace_request(request, profile=Some(args[index + 1])),
command_seen,
)
}
"check" =>
if command_seen {
Err("command specified more than once")
} else {
parse_command_at(
args,
index + 1,
replace_request(request, command=Check),
true,
)
}
"rules" =>
if command_seen {
Err("command specified more than once")
} else {
parse_command_at(
args,
index + 1,
replace_request(request, command=Rules),
true,
)
}
"explain" =>
if command_seen {
Err("command specified more than once")
} else if index + 1 >= args.length() {
Err("explain requires a rule id")
} else {
parse_command_at(
args,
index + 2,
replace_request(request, command=Explain(args[index + 1])),
true,
)
}
_ =>
if token.has_prefix("-") {
Err("unsupported option: " + token)
} else if request.target == "." && request.command == Check {
parse_command_at(
args,
index + 1,
replace_request(request, target=token),
command_seen,
)
} else {
Err("unexpected argument: " + token)
}
}
}
///|
/// Parse process arguments. Omit a subcommand to preserve legacy check usage.
pub fn parse_command_request(
args : Array[String],
) -> Result[CommandRequest, String] {
parse_command_at(args, 1, CommandRequest::default(), false)
}
///|
pub fn command_usage() -> String {
let usage =
#|Usage: moon run cmd/main -- [check] [--format text|json|sarif] [--strict] [--config path] [--profile name] [path]
#| moon run cmd/main -- rules [--format text|json]
#| moon run cmd/main -- explain [--format text|json]
#|
#|Commands:
#| check Analyze a project. This is the default command.
#| rules List the stable rule catalog.
#| explain Show one rule's category, severity, rationale and remediation.
#|
usage
}
///|
pub fn render_rule_explanation(rule_id : String) -> String? {
match find_rule_definition(rule_id) {
Some(rule) =>
Some(
"Rule: " +
rule.id +
"\n" +
"Category: " +
rule.category.label() +
"\n" +
"Default severity: " +
rule.default_severity.label() +
"\n" +
"Title: " +
rule.title +
"\n\n" +
rule.description +
"\n\nSuggested fix: " +
rule.remediation +
"\n",
)
None => None
}
}
///|
pub fn render_rule_explanation_json(rule_id : String) -> String? {
match find_rule_definition(rule_id) {
Some(rule) =>
Some(
Json::object({
"id": Json::string(rule.id),
"category": Json::string(rule.category.label()),
"default_severity": Json::string(rule.default_severity.label()),
"title": Json::string(rule.title),
"description": Json::string(rule.description),
"remediation": Json::string(rule.remediation),
"profiles": Json::array(rule.profiles.map(Json::string)),
}).stringify(),
)
None => None
}
}