specproof 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +283 -0
- data/docs/agent-skills.md +60 -0
- data/docs/benchmark.md +24 -0
- data/docs/constrained-partition-pairs-results.md +44 -0
- data/docs/finite-strings-results.md +75 -0
- data/docs/implementation-status.md +65 -0
- data/docs/input-selection-results.md +123 -0
- data/docs/lint-cleanup-plan.md +12 -0
- data/docs/method-targets-results.md +68 -0
- data/docs/minitest-compatibility.md +33 -0
- data/docs/optional-arguments-plan.md +25 -0
- data/docs/optional-arguments-results.md +45 -0
- data/docs/pairwise-partitions-results.md +53 -0
- data/docs/pairwise-results.md +77 -0
- data/docs/pairwise-suggestions-results.md +104 -0
- data/docs/rbs-implementation-status.md +115 -0
- data/docs/rbs-string-literals-results.md +58 -0
- data/docs/real-project-pilot.md +84 -0
- data/docs/report-format.md +244 -0
- data/examples/age_policy/README.md +39 -0
- data/examples/age_policy/faulty/lib/age_policy.rb +9 -0
- data/examples/age_policy/faulty/test/age_policy_examples_test.rb +17 -0
- data/examples/age_policy/faulty/test/age_policy_test.rb +21 -0
- data/examples/age_policy/lib/age_policy.rb +8 -0
- data/examples/age_policy/run.rb +23 -0
- data/examples/age_policy/specification.rb +8 -0
- data/examples/age_policy/test/age_policy_test.rb +18 -0
- data/examples/constrained_partition_pairs/README.md +40 -0
- data/examples/constrained_partition_pairs/lib/account_eligibility.rb +8 -0
- data/examples/constrained_partition_pairs/requirements.md +11 -0
- data/examples/constrained_partition_pairs/run.rb +22 -0
- data/examples/constrained_partition_pairs/specification.rb +17 -0
- data/examples/constrained_partition_pairs/test_account_eligibility.rb +34 -0
- data/examples/input_selection/README.md +22 -0
- data/examples/input_selection/delivery_policy.rb +11 -0
- data/examples/input_selection/requirements.md +9 -0
- data/examples/input_selection/specification.rb +8 -0
- data/examples/input_selection/test_delivery_policy.rb +38 -0
- data/examples/method_targets/README.md +30 -0
- data/examples/method_targets/lib/delivery_defaults.rb +8 -0
- data/examples/method_targets/lib/delivery_options.rb +12 -0
- data/examples/method_targets/requirements.md +20 -0
- data/examples/method_targets/specification_objects.rb +17 -0
- data/examples/method_targets/specification_strings.rb +15 -0
- data/examples/pairwise/README.md +46 -0
- data/examples/pairwise/constrained_requirements.md +6 -0
- data/examples/pairwise/constrained_specification.rb +11 -0
- data/examples/pairwise/corrected/lib/delivery_options.rb +11 -0
- data/examples/pairwise/corrected/test/delivery_options_test.rb +28 -0
- data/examples/pairwise/faulty/lib/delivery_options.rb +11 -0
- data/examples/pairwise/faulty/test/gap_delivery_options_test.rb +10 -0
- data/examples/pairwise/faulty/test/initial_delivery_options_test.rb +23 -0
- data/examples/pairwise/requirements.md +11 -0
- data/examples/pairwise/run.rb +36 -0
- data/examples/pairwise/specification.rb +9 -0
- data/examples/pairwise/three_boolean/lib/boolean_batch.rb +8 -0
- data/examples/pairwise/three_boolean/test/boolean_batch_test.rb +28 -0
- data/examples/pairwise/three_boolean_specification.rb +10 -0
- data/examples/pairwise_partitions/README.md +34 -0
- data/examples/pairwise_partitions/lib/age_policy.rb +8 -0
- data/examples/pairwise_partitions/requirements.md +8 -0
- data/examples/pairwise_partitions/run.rb +22 -0
- data/examples/pairwise_partitions/specification.rb +13 -0
- data/examples/pairwise_partitions/test_age_policy.rb +29 -0
- data/examples/pairwise_suggestions/README.md +48 -0
- data/examples/pairwise_suggestions/run.rb +55 -0
- data/examples/pairwise_suggestions/three_boolean/lib/suggested_boolean_batch.rb +8 -0
- data/examples/pairwise_suggestions/three_boolean/requirements.md +7 -0
- data/examples/pairwise_suggestions/three_boolean/specification.rb +11 -0
- data/examples/pairwise_suggestions/three_boolean/test/baseline_test.rb +9 -0
- data/examples/pairwise_suggestions/three_boolean/test/complete_test.rb +22 -0
- data/examples/partition_gap.rb +8 -0
- data/examples/rbs_registration/README.md +58 -0
- data/examples/rbs_registration/implementations/age_gt/registration.rb +8 -0
- data/examples/rbs_registration/implementations/age_gte/registration.rb +8 -0
- data/examples/rbs_registration/requirements.md +9 -0
- data/examples/rbs_registration/sig/registration.rbs +3 -0
- data/examples/rbs_registration/specification.rb +10 -0
- data/examples/rbs_registration/test_registration.rb +43 -0
- data/examples/rbs_string_literals/README.md +34 -0
- data/examples/rbs_string_literals/lib/delivery_options.rb +9 -0
- data/examples/rbs_string_literals/requirements.md +11 -0
- data/examples/rbs_string_literals/sig/delivery_options.rbs +3 -0
- data/examples/rbs_string_literals/specification.rb +7 -0
- data/examples/rbs_string_literals/test_delivery_options.rb +46 -0
- data/examples/shipping.rb +12 -0
- data/exe/specproof +7 -0
- data/lib/specproof/checker.rb +144 -0
- data/lib/specproof/cli.rb +292 -0
- data/lib/specproof/collector.rb +304 -0
- data/lib/specproof/combinations.rb +226 -0
- data/lib/specproof/composer.rb +97 -0
- data/lib/specproof/coverage.rb +229 -0
- data/lib/specproof/coverage_report.rb +334 -0
- data/lib/specproof/domains.rb +124 -0
- data/lib/specproof/loader.rb +285 -0
- data/lib/specproof/minitest.rb +443 -0
- data/lib/specproof/model.rb +198 -0
- data/lib/specproof/partition_space.rb +268 -0
- data/lib/specproof/planner.rb +125 -0
- data/lib/specproof/report.rb +248 -0
- data/lib/specproof/skills.rb +188 -0
- data/lib/specproof/sources/rbs.rb +137 -0
- data/lib/specproof/sources/rbs_projector.rb +268 -0
- data/lib/specproof/suggestion_report.rb +159 -0
- data/lib/specproof/suggestions.rb +352 -0
- data/lib/specproof/target_name.rb +100 -0
- data/lib/specproof/type_constraints.rb +76 -0
- data/lib/specproof/version.rb +7 -0
- data/lib/specproof.rb +100 -0
- data/sig/specproof.rbs +222 -0
- data/skills/specproof-specify/SKILL.md +282 -0
- data/skills/specproof-specify/references/usage.md +265 -0
- data/skills/specproof-test-gaps/SKILL.md +214 -0
- data/skills/specproof-test-gaps/references/usage.md +231 -0
- metadata +165 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecProof
|
|
4
|
+
# Finds contradictions between declared domains and partitions.
|
|
5
|
+
class Checker
|
|
6
|
+
def initialize(specifications, criterion: nil, check_combinations: true)
|
|
7
|
+
@specifications = specifications
|
|
8
|
+
@criterion = criterion
|
|
9
|
+
@check_combinations = check_combinations
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def findings
|
|
13
|
+
results = []
|
|
14
|
+
@specifications.each do |specification|
|
|
15
|
+
specification.inputs.each_value do |input|
|
|
16
|
+
check_input(specification, input, results)
|
|
17
|
+
end
|
|
18
|
+
check_combinations(specification, results) if @check_combinations && specification.combination_group
|
|
19
|
+
end
|
|
20
|
+
results.sort_by do |finding|
|
|
21
|
+
if finding.is_a?(CombinationFinding)
|
|
22
|
+
[finding.target, finding.inputs.map(&:to_s), finding.code.to_s]
|
|
23
|
+
else
|
|
24
|
+
[finding.target, [finding.input.to_s], finding.code.to_s, sortable_witness(finding.witness)]
|
|
25
|
+
end
|
|
26
|
+
end.freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def check_combinations(specification, results)
|
|
32
|
+
space = combination_space(specification)
|
|
33
|
+
return unless space.allowed_count.zero?
|
|
34
|
+
|
|
35
|
+
results << CombinationFinding.new(code: :unsatisfiable_combinations, target: specification.target,
|
|
36
|
+
inputs: specification.combination_group.inputs, sources: space.sources)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def combination_space(specification)
|
|
40
|
+
PartitionSpace.for_specification(specification, criterion: @criterion, validate_geometry: false)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def check_input(specification, input, results)
|
|
44
|
+
domain = input.domain
|
|
45
|
+
if Domains.empty?(domain)
|
|
46
|
+
results << finding(:empty_domain, specification, input, Domains.to_a(domain), input.sources)
|
|
47
|
+
end
|
|
48
|
+
input.partitions.each do |partition|
|
|
49
|
+
if Domains.empty?(partition.domain)
|
|
50
|
+
results << finding(:empty_partition, specification, input, Domains.to_a(partition.domain), [partition.source])
|
|
51
|
+
else
|
|
52
|
+
check_containment(specification, input, partition, results)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
if domain.is_a?(IntegerDomain)
|
|
56
|
+
check_integer_family(specification, input, results) unless Domains.empty?(domain)
|
|
57
|
+
else
|
|
58
|
+
check_finite_family(specification, input, results)
|
|
59
|
+
end
|
|
60
|
+
check_type_domain(specification, input, results) if input.type_constraint
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def check_containment(specification, input, partition, results)
|
|
64
|
+
domain = input.domain
|
|
65
|
+
child = partition.domain
|
|
66
|
+
outside = if domain.is_a?(IntegerDomain)
|
|
67
|
+
ranges = []
|
|
68
|
+
ranges << [child.lower, [child.upper, domain.lower - 1].min] if child.lower < domain.lower
|
|
69
|
+
ranges << [[child.lower, domain.upper + 1].max, child.upper] if child.upper > domain.upper
|
|
70
|
+
ranges.reject { |left, right| left > right }
|
|
71
|
+
else
|
|
72
|
+
child.values.reject { |value| domain.values.include?(value) }
|
|
73
|
+
end
|
|
74
|
+
return if outside.empty?
|
|
75
|
+
|
|
76
|
+
witness = outside.first
|
|
77
|
+
results << finding(:partition_outside_domain, specification, input, witness, input.sources + [partition.source])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def check_integer_family(specification, input, results)
|
|
81
|
+
partitions = input.partitions.select { |part| part.domain.is_a?(IntegerDomain) && !Domains.empty?(part.domain) }
|
|
82
|
+
if input.disjoint
|
|
83
|
+
partitions.each_with_index do |left, index|
|
|
84
|
+
partitions.drop(index + 1).each do |right|
|
|
85
|
+
lower = [left.domain.lower, right.domain.lower].max
|
|
86
|
+
upper = [left.domain.upper, right.domain.upper].min
|
|
87
|
+
next if lower > upper
|
|
88
|
+
|
|
89
|
+
results << finding(:partition_overlap, specification, input, lower, [left.source, right.source])
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
return unless input.exhaustive
|
|
94
|
+
|
|
95
|
+
cursor = input.domain.lower
|
|
96
|
+
partitions.map(&:domain).sort_by { |part| [part.lower, part.upper] }.each do |part|
|
|
97
|
+
lower = [part.lower, input.domain.lower].max
|
|
98
|
+
upper = [part.upper, input.domain.upper].min
|
|
99
|
+
next if lower > upper
|
|
100
|
+
|
|
101
|
+
results << finding(:partition_gap, specification, input, [cursor, lower - 1], input.sources) if cursor < lower
|
|
102
|
+
cursor = [cursor, upper + 1].max
|
|
103
|
+
end
|
|
104
|
+
return unless cursor <= input.domain.upper
|
|
105
|
+
|
|
106
|
+
results << finding(:partition_gap, specification, input, [cursor, input.domain.upper], input.sources)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def check_finite_family(specification, input, results)
|
|
110
|
+
partitions = input.partitions.select { |part| part.domain.is_a?(FiniteDomain) && !part.domain.values.empty? }
|
|
111
|
+
if input.disjoint
|
|
112
|
+
partitions.each_with_index do |left, index|
|
|
113
|
+
partitions.drop(index + 1).each do |right|
|
|
114
|
+
common = left.domain.values & right.domain.values
|
|
115
|
+
common.each do |value|
|
|
116
|
+
results << finding(:partition_overlap, specification, input, value, [left.source, right.source])
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
return unless input.exhaustive
|
|
122
|
+
|
|
123
|
+
covered = partitions.flat_map { |part| part.domain.values }.uniq
|
|
124
|
+
(input.domain.values - covered).each do |value|
|
|
125
|
+
results << finding(:partition_gap, specification, input, value, input.sources)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def check_type_domain(specification, input, results)
|
|
130
|
+
witness = TypeConstraints.first_excluded(input.type_constraint, input.domain)
|
|
131
|
+
return if witness.equal?(TypeConstraints::NO_EXCLUSION)
|
|
132
|
+
|
|
133
|
+
results << finding(:type_domain_conflict, specification, input, witness, input.sources)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def finding(code, specification, input, witness, sources)
|
|
137
|
+
Finding.new(code: code, target: specification.target, input: input.name, witness: witness, sources: sources)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def sortable_witness(witness)
|
|
141
|
+
witness.is_a?(Array) ? [0, *witness] : [1, *Domains.sort_key(witness)]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "suggestion_report"
|
|
5
|
+
|
|
6
|
+
module SpecProof
|
|
7
|
+
# Parses command arguments and dispatches SpecProof commands.
|
|
8
|
+
class CLI
|
|
9
|
+
USAGE = <<~TEXT
|
|
10
|
+
Usage:
|
|
11
|
+
specproof --version
|
|
12
|
+
specproof check [SPECIFICATION.rb ...] [--rbs FILE] [--target TARGET] [--format human|json]
|
|
13
|
+
specproof plan [SPECIFICATION.rb ...] [--rbs FILE] [--target TARGET] [--criterion NAME] [--format human|json]
|
|
14
|
+
specproof suggest SPECIFICATION.rb ... --report COVERAGE.json [--rbs FILE --target TARGET] [--format human|json]
|
|
15
|
+
specproof skills export --to DIRECTORY [--format human|json]
|
|
16
|
+
TEXT
|
|
17
|
+
|
|
18
|
+
def initialize(stdout: $stdout, stderr: $stderr)
|
|
19
|
+
@stdout = stdout
|
|
20
|
+
@stderr = stderr
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run(argv)
|
|
24
|
+
args = argv.dup
|
|
25
|
+
if [["--version"], ["-v"]].include?(args)
|
|
26
|
+
@stdout.puts("SpecProof #{VERSION}")
|
|
27
|
+
return 0
|
|
28
|
+
end
|
|
29
|
+
if args.empty? || args == ["--help"] || args == ["-h"] || args == ["help"]
|
|
30
|
+
@stdout.write(USAGE)
|
|
31
|
+
return 0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
command = args.shift
|
|
35
|
+
return run_skills(args) if command == "skills"
|
|
36
|
+
return run_specifications(command, args) if %w[check plan suggest].include?(command)
|
|
37
|
+
|
|
38
|
+
unknown_command(args, "unknown_command", "unsupported command #{command.inspect}", kind: "error")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def run_skills(args)
|
|
44
|
+
unless args.shift == "export"
|
|
45
|
+
return unknown_command(args, "unknown_skills_command", "expected skills export", kind: "skills_export")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
destination, format, option_error = parse_skill_options(args)
|
|
49
|
+
return emit_error(option_error, format, kind: "skills_export") if option_error
|
|
50
|
+
|
|
51
|
+
with_errors("skills_export", format) do
|
|
52
|
+
result = Skills.export(to: destination)
|
|
53
|
+
if format == "json"
|
|
54
|
+
@stdout.puts(JSON.generate(Report.envelope(kind: "skills_export").merge(result)))
|
|
55
|
+
elsif result.fetch(:exported).empty?
|
|
56
|
+
@stdout.puts("Skills already match the export at #{result.fetch(:destination)}.")
|
|
57
|
+
else
|
|
58
|
+
@stdout.puts("Exported #{result.fetch(:exported).length} skill files to #{result.fetch(:destination)}.")
|
|
59
|
+
end
|
|
60
|
+
0
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def run_specifications(kind, args)
|
|
65
|
+
paths, rbs_paths, targets, criterion, format, option_error, report_path = parse_options(args, kind: kind)
|
|
66
|
+
report_kind = kind == "suggest" ? "suggestions" : kind
|
|
67
|
+
if option_error
|
|
68
|
+
return emit_error(option_error, format, kind: report_kind,
|
|
69
|
+
criterion: kind == "plan" ? criterion : nil)
|
|
70
|
+
end
|
|
71
|
+
if paths.empty? && rbs_paths.empty? && targets.empty?
|
|
72
|
+
return emit_error(error("paths_required", "provide specification paths or an RBS selection"), format,
|
|
73
|
+
kind: report_kind, criterion: kind == "plan" ? criterion : nil)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
with_errors(report_kind, format, criterion: kind == "plan" ? criterion : nil) do
|
|
77
|
+
specifications = with_loader_output(format) do
|
|
78
|
+
if rbs_paths.empty? && targets.empty?
|
|
79
|
+
SpecProof.load(paths)
|
|
80
|
+
else
|
|
81
|
+
SpecProof.load(paths, rbs: rbs_paths, targets: targets)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
case kind
|
|
85
|
+
when "check" then run_check(specifications, format)
|
|
86
|
+
when "plan" then run_plan(specifications, format, criterion)
|
|
87
|
+
else SuggestionReport.execute(specifications: specifications, path: report_path, format: format,
|
|
88
|
+
stdout: @stdout)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def run_check(specifications, format)
|
|
94
|
+
findings = SpecProof.check(specifications)
|
|
95
|
+
if format == "json"
|
|
96
|
+
@stdout.puts(JSON.generate(Report.check(specifications: specifications, findings: findings)))
|
|
97
|
+
else
|
|
98
|
+
if findings.empty?
|
|
99
|
+
@stdout.puts("No specification findings.")
|
|
100
|
+
else
|
|
101
|
+
findings.each { |finding| @stdout.puts(Report.finding(finding)) }
|
|
102
|
+
end
|
|
103
|
+
@stdout.puts(Report.human_scope(specifications))
|
|
104
|
+
end
|
|
105
|
+
findings.empty? ? 0 : 1
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def run_plan(specifications, format, criterion)
|
|
109
|
+
obligations = SpecProof.plan(specifications, criterion: criterion)
|
|
110
|
+
if format == "json"
|
|
111
|
+
@stdout.puts(JSON.generate(Report.plan(specifications: specifications, obligations: obligations,
|
|
112
|
+
criterion: criterion)))
|
|
113
|
+
else
|
|
114
|
+
@stdout.puts(Report.human_plan(obligations, criterion: criterion, specifications: specifications))
|
|
115
|
+
end
|
|
116
|
+
0
|
|
117
|
+
rescue InvalidSpecificationError => e
|
|
118
|
+
if format == "json"
|
|
119
|
+
payload = Report.plan(specifications: specifications, obligations: [], criterion: criterion)
|
|
120
|
+
payload[:findings] = e.findings.map { |finding| Report.finding_hash(finding) }
|
|
121
|
+
@stdout.puts(JSON.generate(payload))
|
|
122
|
+
else
|
|
123
|
+
e.findings.each { |finding| @stdout.puts(Report.finding(finding)) }
|
|
124
|
+
@stdout.puts(Report.human_scope(specifications))
|
|
125
|
+
end
|
|
126
|
+
1
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def unknown_command(args, code, message, kind:)
|
|
130
|
+
return emit_error(error(code, message), "json", kind: kind) if json_format_requested?(args)
|
|
131
|
+
|
|
132
|
+
@stderr.write(USAGE)
|
|
133
|
+
2
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def with_errors(kind, format, criterion: nil)
|
|
137
|
+
yield
|
|
138
|
+
rescue ConfigurationError => e
|
|
139
|
+
emit_error(error(e.code, e.message, path: e.path, source: e.source), format,
|
|
140
|
+
kind: kind, criterion: criterion)
|
|
141
|
+
rescue SyntaxError => e
|
|
142
|
+
emit_error(error("syntax_error", e.message, path: e.path), format, kind: kind, criterion: criterion)
|
|
143
|
+
rescue SystemCallError, IOError => e
|
|
144
|
+
emit_error(error("io_error", e.message), format, kind: kind, criterion: criterion)
|
|
145
|
+
rescue StandardError => e
|
|
146
|
+
emit_error(error("specification_error", e.message), format, kind: kind, criterion: criterion)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def parse_options(args, kind:)
|
|
150
|
+
options = { format: "human", criterion: "input-basics-v1", criterion_seen: false,
|
|
151
|
+
paths: [], rbs_paths: [], targets: [], report_paths: [], parse_error: nil }
|
|
152
|
+
while (argument = args.shift)
|
|
153
|
+
parse_error = parse_option(argument, args, options, kind)
|
|
154
|
+
options[:parse_error] ||= parse_error
|
|
155
|
+
end
|
|
156
|
+
if kind == "suggest" && options[:report_paths].empty?
|
|
157
|
+
options[:parse_error] ||= error("missing_coverage_report", "suggest requires one --report COVERAGE.json")
|
|
158
|
+
end
|
|
159
|
+
[options[:paths], options[:rbs_paths], options[:targets], options[:criterion], options[:format],
|
|
160
|
+
options[:parse_error], options[:report_paths].first]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def parse_option(argument, args, options, kind)
|
|
164
|
+
case argument
|
|
165
|
+
when "--format" then parse_format_option(args, options)
|
|
166
|
+
when "--criterion" then parse_criterion_option(args, options, kind)
|
|
167
|
+
when "--report" then SuggestionReport.parse_report_option(args: args, options: options, kind: kind)
|
|
168
|
+
when "--rbs", "--target" then parse_selection_option(argument, args, options)
|
|
169
|
+
else parse_path_option(argument, options)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def parse_format_option(args, options)
|
|
174
|
+
value = args.shift
|
|
175
|
+
if %w[human json].include?(value)
|
|
176
|
+
options[:format] = value
|
|
177
|
+
nil
|
|
178
|
+
else
|
|
179
|
+
args.unshift(value) if value
|
|
180
|
+
error("invalid_format", "--format requires human or json")
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def parse_criterion_option(args, options, kind)
|
|
185
|
+
value, parse_error = option_value("--criterion", args)
|
|
186
|
+
return parse_error unless value
|
|
187
|
+
return error("unsupported_option", "--criterion is available only for plan") unless kind == "plan"
|
|
188
|
+
return error("duplicate_criterion", "specify --criterion only once") if options[:criterion_seen]
|
|
189
|
+
|
|
190
|
+
options[:criterion] = value
|
|
191
|
+
options[:criterion_seen] = true
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def parse_selection_option(argument, args, options)
|
|
196
|
+
value, parse_error = option_value(argument, args)
|
|
197
|
+
return parse_error unless value
|
|
198
|
+
|
|
199
|
+
options.fetch(argument == "--rbs" ? :rbs_paths : :targets) << value
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def option_value(argument, args)
|
|
204
|
+
value = args.shift
|
|
205
|
+
if value.nil? || value.empty? || value.start_with?("-")
|
|
206
|
+
args.unshift(value) if value
|
|
207
|
+
[nil, error("missing_option_value", "#{argument} requires a value")]
|
|
208
|
+
else
|
|
209
|
+
[value, nil]
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def parse_path_option(argument, options)
|
|
214
|
+
if argument.start_with?("-")
|
|
215
|
+
error("unsupported_option", "unsupported option #{argument.inspect}")
|
|
216
|
+
else
|
|
217
|
+
options[:paths] << argument
|
|
218
|
+
nil
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def json_format_requested?(args)
|
|
223
|
+
index = args.index("--format")
|
|
224
|
+
index && args[index + 1] == "json"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def parse_skill_options(args)
|
|
228
|
+
destination = nil
|
|
229
|
+
format = "human"
|
|
230
|
+
parse_error = nil
|
|
231
|
+
destination_seen = false
|
|
232
|
+
|
|
233
|
+
while (arg = args.shift)
|
|
234
|
+
case arg
|
|
235
|
+
when "--to"
|
|
236
|
+
value = args.shift
|
|
237
|
+
if destination_seen
|
|
238
|
+
parse_error ||= error("duplicate_destination", "specify --to only once")
|
|
239
|
+
elsif value.nil? || value.start_with?("--")
|
|
240
|
+
args.unshift(value) if value
|
|
241
|
+
parse_error ||= error("missing_skills_destination", "--to requires a destination")
|
|
242
|
+
else
|
|
243
|
+
destination = value
|
|
244
|
+
destination_seen = true
|
|
245
|
+
end
|
|
246
|
+
when "--format"
|
|
247
|
+
value = args.shift
|
|
248
|
+
if %w[human json].include?(value)
|
|
249
|
+
format = value
|
|
250
|
+
else
|
|
251
|
+
parse_error ||= error("invalid_format", "--format requires human or json")
|
|
252
|
+
end
|
|
253
|
+
else
|
|
254
|
+
parse_error ||= error("unsupported_option", "unsupported skill export argument #{arg.inspect}")
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
unless destination || parse_error
|
|
259
|
+
parse_error = error("missing_skills_destination", "provide an explicit destination with --to")
|
|
260
|
+
end
|
|
261
|
+
[destination, format, parse_error]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def with_loader_output(format)
|
|
265
|
+
return yield unless format == "json"
|
|
266
|
+
|
|
267
|
+
original = $stdout
|
|
268
|
+
$stdout = @stderr
|
|
269
|
+
yield
|
|
270
|
+
ensure
|
|
271
|
+
$stdout = original if format == "json"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def error(code, message, path: nil, source: nil)
|
|
275
|
+
result = { code: code.to_s, message: message.to_s }
|
|
276
|
+
result[:path] = path if path
|
|
277
|
+
result[:source] = Report.source(source) if source
|
|
278
|
+
result
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def emit_error(details, format, kind: "error", criterion: nil)
|
|
282
|
+
if format == "json"
|
|
283
|
+
payload = Report.envelope(kind: kind, errors: [details])
|
|
284
|
+
payload[:criterion] = criterion if criterion
|
|
285
|
+
@stdout.puts(JSON.generate(payload))
|
|
286
|
+
else
|
|
287
|
+
@stderr.puts("#{details[:code]}: #{details[:message]}")
|
|
288
|
+
end
|
|
289
|
+
2
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|