mutineer 0.7.0 → 0.9.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/README.md +39 -4
  4. data/lib/mutineer/baseline.rb +29 -13
  5. data/lib/mutineer/changed_lines.rb +29 -12
  6. data/lib/mutineer/cli.rb +88 -20
  7. data/lib/mutineer/config.rb +24 -2
  8. data/lib/mutineer/coverage_map.rb +86 -4
  9. data/lib/mutineer/isolation.rb +89 -40
  10. data/lib/mutineer/minitest_integration.rb +13 -9
  11. data/lib/mutineer/mutant_id.rb +24 -5
  12. data/lib/mutineer/mutation.rb +12 -6
  13. data/lib/mutineer/mutator_registry.rb +33 -6
  14. data/lib/mutineer/mutators/arithmetic.rb +8 -2
  15. data/lib/mutineer/mutators/base.rb +11 -3
  16. data/lib/mutineer/mutators/boolean_connector.rb +15 -5
  17. data/lib/mutineer/mutators/boolean_literal.rb +19 -6
  18. data/lib/mutineer/mutators/collection_method.rb +44 -0
  19. data/lib/mutineer/mutators/comparison.rb +7 -8
  20. data/lib/mutineer/mutators/condition_negation.rb +14 -5
  21. data/lib/mutineer/mutators/literal_mutation.rb +15 -4
  22. data/lib/mutineer/mutators/regex_literal.rb +72 -0
  23. data/lib/mutineer/mutators/return_nil.rb +22 -7
  24. data/lib/mutineer/mutators/statement_removal.rb +6 -9
  25. data/lib/mutineer/mutators/string_literal.rb +34 -0
  26. data/lib/mutineer/pairing.rb +32 -13
  27. data/lib/mutineer/parser.rb +17 -7
  28. data/lib/mutineer/project.rb +73 -2
  29. data/lib/mutineer/reporter.rb +169 -0
  30. data/lib/mutineer/result.rb +100 -32
  31. data/lib/mutineer/runner.rb +29 -2
  32. data/lib/mutineer/subject.rb +10 -6
  33. data/lib/mutineer/test_runners/minitest.rb +5 -4
  34. data/lib/mutineer/test_runners/rspec.rb +10 -17
  35. data/lib/mutineer/test_runners.rb +9 -2
  36. data/lib/mutineer/version.rb +2 -1
  37. data/lib/mutineer/worker_pool.rb +51 -29
  38. data/lib/mutineer.rb +4 -0
  39. metadata +18 -1
@@ -4,20 +4,26 @@ require_relative "base"
4
4
 
5
5
  module Mutineer
6
6
  module Mutators
7
- # Boolean connector operator: && <-> ||, and <-> or. Replacement is derived
8
- # from the actual source token (operator_loc.slice) so symbolic and keyword
9
- # forms each map to their own form — never crossing && to `or`, which would
10
- # change precedence and surprise the reader (KTD-2).
7
+ # Boolean connector mutator.
11
8
  #
12
- # Clean-room: from the spec's operator table, not the mutant gem.
9
+ # Replaces symbolic and keyword connectors with their opposite form.
13
10
  class BooleanConnector < Base
11
+ # Token replacements for boolean connectors.
14
12
  REPLACEMENTS = { "&&" => "||", "||" => "&&", "and" => "or", "or" => "and" }.freeze
15
13
 
14
+ # Visits `and` nodes.
15
+ #
16
+ # @param node [Prism::AndNode] node to inspect.
17
+ # @return [void]
16
18
  def visit_and_node(node)
17
19
  emit(node)
18
20
  super
19
21
  end
20
22
 
23
+ # Visits `or` nodes.
24
+ #
25
+ # @param node [Prism::OrNode] node to inspect.
26
+ # @return [void]
21
27
  def visit_or_node(node)
22
28
  emit(node)
23
29
  super
@@ -25,6 +31,10 @@ module Mutineer
25
31
 
26
32
  private
27
33
 
34
+ # Emits a connector mutation when the token is replaceable.
35
+ #
36
+ # @param node [Prism::Node] connector node.
37
+ # @return [void]
28
38
  def emit(node)
29
39
  loc = node.operator_loc
30
40
  replacement = REPLACEMENTS[loc.slice]
@@ -4,24 +4,32 @@ require_relative "base"
4
4
 
5
5
  module Mutineer
6
6
  module Mutators
7
- # Mutates true/false AND nil literals — "boolean_literal" is the spec's name
8
- # for the family (§4), so nil is in-scope by design even though it is not
9
- # strictly a boolean. true<->false, and nil->true (nil->true catches more
10
- # return-value gaps than nil->false). Rewrites the whole node location;
11
- # these nodes have no sub-token location.
7
+ # Boolean literal mutator.
12
8
  #
13
- # Clean-room: from the spec's operator table, not the mutant gem.
9
+ # Mutates true/false and nil literals in the boolean_literal family.
14
10
  class BooleanLiteral < Base
11
+ # Visits true literals.
12
+ #
13
+ # @param node [Prism::TrueNode] node to inspect.
14
+ # @return [void]
15
15
  def visit_true_node(node)
16
16
  emit(node, "false")
17
17
  super
18
18
  end
19
19
 
20
+ # Visits false literals.
21
+ #
22
+ # @param node [Prism::FalseNode] node to inspect.
23
+ # @return [void]
20
24
  def visit_false_node(node)
21
25
  emit(node, "true")
22
26
  super
23
27
  end
24
28
 
29
+ # Visits nil literals.
30
+ #
31
+ # @param node [Prism::NilNode] node to inspect.
32
+ # @return [void]
25
33
  def visit_nil_node(node)
26
34
  emit(node, "true")
27
35
  super
@@ -29,6 +37,11 @@ module Mutineer
29
37
 
30
38
  private
31
39
 
40
+ # Emits a boolean-literal mutation.
41
+ #
42
+ # @param node [Prism::Node] literal node.
43
+ # @param replacement [String] replacement token.
44
+ # @return [void]
32
45
  def emit(node, replacement)
33
46
  loc = node.location
34
47
  @mutations << Mutation.new(
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Collection / enumerable method-name mutator (Tier-2).
8
+ #
9
+ # Swaps the method-name token of a call to its semantic opposite, one
10
+ # mutation per occurrence, exactly like the arithmetic/comparison operators.
11
+ class CollectionMethod < Base
12
+ # Method-name swaps. All targets are core-Ruby Enumerable/Array methods so
13
+ # the mutant exercises real behaviour rather than always raising.
14
+ #
15
+ # ponytail: include? -> exclude? was specced but skipped — exclude? is not
16
+ # core Ruby, so that mutant would always NoMethodError (a weak mutant).
17
+ REPLACEMENTS = {
18
+ map: "each", each: "map",
19
+ all?: "any?", any?: "all?",
20
+ first: "last", last: "first",
21
+ min: "max", max: "min",
22
+ select: "reject", reject: "select"
23
+ }.freeze
24
+
25
+ # Visits call nodes and emits collection-method mutations.
26
+ #
27
+ # @param node [Prism::CallNode] call node to inspect.
28
+ # @return [void]
29
+ def visit_call_node(node)
30
+ replacement = REPLACEMENTS[node.name]
31
+ loc = node.message_loc
32
+ if replacement && loc
33
+ @mutations << Mutation.new(
34
+ start_offset: loc.start_offset,
35
+ end_offset: loc.end_offset,
36
+ replacement: replacement,
37
+ operator: :collection_method
38
+ )
39
+ end
40
+ super
41
+ end
42
+ end
43
+ end
44
+ end
@@ -4,23 +4,22 @@ require_relative "base"
4
4
 
5
5
  module Mutineer
6
6
  module Mutators
7
- # Comparison / boundary operator: <->-<=, >->-=>, ==->!=, etc. The single
8
- # highest-value Tier-1 family (spec §4) — exposes off-by-one and boundary
9
- # gaps line coverage never catches. Rewrites the operator token
10
- # (CallNode#message_loc), one mutation per occurrence.
7
+ # Comparison and boundary mutator.
11
8
  #
12
- # Clean-room: implemented from the spec's operator table and public
13
- # mutation-testing literature, not the mutant gem.
9
+ # Rewrites comparison operators one occurrence at a time.
14
10
  class Comparison < Base
11
+ # Token replacements for comparison operators.
15
12
  REPLACEMENTS = {
16
13
  :< => "<=", :<= => "<", :> => ">=", :>= => ">", :== => "!=", :!= => "=="
17
14
  }.freeze
18
15
 
16
+ # Visits call nodes and emits comparison mutations.
17
+ #
18
+ # @param node [Prism::CallNode] call node to inspect.
19
+ # @return [void]
19
20
  def visit_call_node(node)
20
21
  replacement = REPLACEMENTS[node.name]
21
22
  loc = node.message_loc
22
- # receiver guard: reject any unary call accidentally named like an
23
- # operator; binary comparisons always have a receiver.
24
23
  if replacement && loc && node.receiver
25
24
  @mutations << Mutation.new(
26
25
  start_offset: loc.start_offset,
@@ -4,18 +4,23 @@ require_relative "base"
4
4
 
5
5
  module Mutineer
6
6
  module Mutators
7
- # Condition-negation operator (Tier 2, OFF by default). Wraps an if/unless/
8
- # ternary condition in `!( ... )` textually. Ruby ternaries parse as IfNode in
9
- # Prism, so visit_if_node covers them too (R12). The standard validity re-parse
10
- # downstream discards any wrap that fails to round-trip (R14).
7
+ # Condition-negation mutator.
11
8
  #
12
- # Clean-room: from the spec's operator description, not the mutant gem.
9
+ # Wraps if/unless predicates in `!( ... )` textually.
13
10
  class ConditionNegation < Base
11
+ # Visits if nodes.
12
+ #
13
+ # @param node [Prism::IfNode] node to inspect.
14
+ # @return [void]
14
15
  def visit_if_node(node)
15
16
  wrap(node.predicate)
16
17
  super
17
18
  end
18
19
 
20
+ # Visits unless nodes.
21
+ #
22
+ # @param node [Prism::UnlessNode] node to inspect.
23
+ # @return [void]
19
24
  def visit_unless_node(node)
20
25
  wrap(node.predicate)
21
26
  super
@@ -23,6 +28,10 @@ module Mutineer
23
28
 
24
29
  private
25
30
 
31
+ # Wraps a predicate in negation.
32
+ #
33
+ # @param predicate [Prism::Node, nil] predicate node.
34
+ # @return [void]
26
35
  def wrap(predicate)
27
36
  return unless predicate
28
37
 
@@ -4,12 +4,14 @@ require_relative "base"
4
4
 
5
5
  module Mutineer
6
6
  module Mutators
7
- # Literal-fuzzing operator (Tier 2, OFF by default). Integers emit up to
8
- # three mutations (0, 1, n+1) with no-op guards for 0 and 1; strings collapse
9
- # to "" unless already empty. One mutation per emitted candidate (R11).
7
+ # Literal mutation mutator.
10
8
  #
11
- # Clean-room: from the spec's operator description, not the mutant gem.
9
+ # Mutates integers and strings with the Tier-2 literal rules.
12
10
  class LiteralMutation < Base
11
+ # Visits integer literals.
12
+ #
13
+ # @param node [Prism::IntegerNode] node to inspect.
14
+ # @return [void]
13
15
  def visit_integer_node(node)
14
16
  n = node.value
15
17
  emit(node.location, "0") unless n.zero?
@@ -18,6 +20,10 @@ module Mutineer
18
20
  super
19
21
  end
20
22
 
23
+ # Visits string literals.
24
+ #
25
+ # @param node [Prism::StringNode] node to inspect.
26
+ # @return [void]
21
27
  def visit_string_node(node)
22
28
  loc = node.location
23
29
  token = @source.byteslice(loc.start_offset...loc.end_offset)
@@ -27,6 +33,11 @@ module Mutineer
27
33
 
28
34
  private
29
35
 
36
+ # Emits a literal mutation.
37
+ #
38
+ # @param loc [Prism::Location] source location.
39
+ # @param replacement [String] replacement literal.
40
+ # @return [void]
30
41
  def emit(loc, replacement)
31
42
  @mutations << Mutation.new(
32
43
  start_offset: loc.start_offset,
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Regular-expression literal mutator (Tier-2).
8
+ #
9
+ # Conservative, clearly-safe textual swaps inside a plain regex literal:
10
+ # drop a leading +^+ anchor, drop a trailing +$+ anchor, and swap the +
11
+ # and * quantifiers. Interpolated regexes use a different node type and are
12
+ # never visited. Escaped tokens (preceded by a backslash) are left alone.
13
+ class RegexLiteral < Base
14
+ # Visits regular-expression literals.
15
+ #
16
+ # @param node [Prism::RegularExpressionNode] node to inspect.
17
+ # @return [void]
18
+ def visit_regular_expression_node(node)
19
+ loc = node.content_loc
20
+ content = @source.byteslice(loc.start_offset...loc.end_offset)
21
+ scan(content, loc.start_offset)
22
+ super
23
+ end
24
+
25
+ private
26
+
27
+ # Scans the pattern bytes and emits anchor/quantifier mutations.
28
+ #
29
+ # @param content [String] regex pattern between the slashes.
30
+ # @param base [Integer] byte offset of the pattern start in source.
31
+ # @return [void]
32
+ def scan(content, base)
33
+ emit(base, base + 1, "") if content.start_with?("^")
34
+ emit(base + content.bytesize - 1, base + content.bytesize, "") if trailing_anchor?(content)
35
+
36
+ escaped = false
37
+ offset = base
38
+ content.each_char do |ch|
39
+ unless escaped
40
+ emit(offset, offset + 1, "*") if ch == "+"
41
+ emit(offset, offset + 1, "+") if ch == "*"
42
+ end
43
+ escaped = ch == "\\" && !escaped
44
+ offset += ch.bytesize
45
+ end
46
+ end
47
+
48
+ # Whether the pattern ends with an unescaped +$+ anchor.
49
+ #
50
+ # @param content [String] regex pattern.
51
+ # @return [Boolean] true when a trailing anchor is droppable.
52
+ def trailing_anchor?(content)
53
+ content.end_with?("$") && !content.end_with?("\\$")
54
+ end
55
+
56
+ # Emits a regex mutation.
57
+ #
58
+ # @param start_offset [Integer] byte start.
59
+ # @param end_offset [Integer] byte end.
60
+ # @param replacement [String] replacement text.
61
+ # @return [void]
62
+ def emit(start_offset, end_offset, replacement)
63
+ @mutations << Mutation.new(
64
+ start_offset: start_offset,
65
+ end_offset: end_offset,
66
+ replacement: replacement,
67
+ operator: :regex
68
+ )
69
+ end
70
+ end
71
+ end
72
+ end
@@ -4,15 +4,15 @@ require_relative "base"
4
4
 
5
5
  module Mutineer
6
6
  module Mutators
7
- # Return-value-nil operator (Tier 2, OFF by default). Two rules:
8
- # 1. an explicit `return <expr>` -> `return nil`, unless the value is
9
- # already nil (no-op guard).
10
- # 2. a method body whose final expression is neither a ReturnNode nor a
11
- # NilNode -> that expression becomes `nil`.
12
- # Nested defs are their own subjects, so we never descend into them (R10).
7
+ # Return-nil mutator.
13
8
  #
14
- # Clean-room: from the spec's operator description, not the mutant gem.
9
+ # Replaces explicit return values and final expressions with nil.
15
10
  class ReturnNil < Base
11
+ # Collects return-nil mutations for a subject.
12
+ #
13
+ # @param subject [Mutineer::Subject] subject to inspect.
14
+ # @param source [String] full source text.
15
+ # @return [Array<Mutineer::Mutation>] collected mutations.
16
16
  def mutations_for(subject, source)
17
17
  @source = source
18
18
  @mutations = []
@@ -24,6 +24,10 @@ module Mutineer
24
24
  @mutations
25
25
  end
26
26
 
27
+ # Visits return nodes.
28
+ #
29
+ # @param node [Prism::ReturnNode] node to inspect.
30
+ # @return [void]
27
31
  def visit_return_node(node)
28
32
  args = node.arguments
29
33
  if args
@@ -37,10 +41,17 @@ module Mutineer
37
41
 
38
42
  # Nested method definitions are discovered as their own subjects; do not
39
43
  # recurse into them (prevents double-counting their statements).
44
+ #
45
+ # @param node [Prism::DefNode] nested definition node.
46
+ # @return [void]
40
47
  def visit_def_node(node); end
41
48
 
42
49
  private
43
50
 
51
+ # Mutates a method's final expression to nil when eligible.
52
+ #
53
+ # @param body [Prism::Node] method body node.
54
+ # @return [void]
44
55
  def final_expression_nil(body)
45
56
  return unless body.is_a?(Prism::StatementsNode)
46
57
 
@@ -50,6 +61,10 @@ module Mutineer
50
61
  emit(last.location)
51
62
  end
52
63
 
64
+ # Emits a return-nil mutation.
65
+ #
66
+ # @param loc [Prism::Location] source location.
67
+ # @return [void]
53
68
  def emit(loc)
54
69
  @mutations << Mutation.new(
55
70
  start_offset: loc.start_offset,
@@ -4,14 +4,14 @@ require_relative "base"
4
4
 
5
5
  module Mutineer
6
6
  module Mutators
7
- # Statement-removal operator: replace each non-final method statement with
8
- # "nil". Tests whether the suite detects a missing side effect. The final
9
- # expression is always skipped — replacing the return value with nil is the
10
- # M5 return-nil operator's distinct concern (KTD-1). A body with < 2
11
- # statements has no non-final statement, so it generates nothing.
7
+ # Statement-removal mutator.
12
8
  #
13
- # Clean-room: from the spec's operator description, not the mutant gem.
9
+ # Replaces each non-final method statement with `nil`.
14
10
  class StatementRemoval < Base
11
+ # Visits statement nodes and emits removals.
12
+ #
13
+ # @param node [Prism::StatementsNode] statement list to inspect.
14
+ # @return [void]
15
15
  def visit_statements_node(node)
16
16
  stmts = node.body
17
17
  return if stmts.length < 2
@@ -25,9 +25,6 @@ module Mutineer
25
25
  operator: :statement_removal
26
26
  )
27
27
  end
28
- # ponytail: no super — recursing into a nested StatementsNode would
29
- # re-emit removals already covered at the top level and double-count.
30
- # Each subject's body is visited once.
31
28
  end
32
29
  end
33
30
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # String literal mutator (Tier-2).
8
+ #
9
+ # Empties a non-empty string literal and fills an empty one. Only plain
10
+ # quoted strings are touched; interpolated strings, heredocs and %-literals
11
+ # are skipped for safety (they re-parse unpredictably).
12
+ class StringLiteral < Base
13
+ # Visits string literals.
14
+ #
15
+ # @param node [Prism::StringNode] node to inspect.
16
+ # @return [void]
17
+ def visit_string_node(node)
18
+ # ponytail: only plain "..." / '...' quotes. opening is nil for
19
+ # interpolation parts and %w[] elements; heredocs/%-literals use a
20
+ # different opening token. Skipping them keeps mutants re-parseable.
21
+ if %w[" '].include?(node.opening)
22
+ loc = node.content_loc
23
+ @mutations << Mutation.new(
24
+ start_offset: loc.start_offset,
25
+ end_offset: loc.end_offset,
26
+ replacement: node.unescaped.empty? ? "mutineer" : "",
27
+ operator: :string_literal
28
+ )
29
+ end
30
+ super
31
+ end
32
+ end
33
+ end
34
+ end
@@ -4,18 +4,24 @@ module Mutineer
4
4
  # Source -> test pairing by path convention (#11). Pure stdlib path logic:
5
5
  # no Rails, no class loading, no process. Two jobs:
6
6
  # * expand_sources — a directory argument becomes its sorted **/*.rb files.
7
- # * infer_test — a source's test file by convention (app/ and lib/ sources
8
- # map to test/.../_test.rb or spec/.../_spec.rb), preserving
9
- # namespaced subdirectories. First EXISTING candidate wins.
7
+ # * infer_test — a source's test file by convention (app/ and lib/
8
+ # sources map to test/.../_test.rb or spec/.../_spec.rb),
9
+ # preserving namespaced subdirectories. First EXISTING
10
+ # candidate wins.
10
11
  #
11
- # Independently unit-testable: every method is pure in/out over the filesystem,
12
- # so the pairing contract is exercised with plain fixtures, no Rails, no fork.
12
+ # Independently unit-testable: every method is pure in/out over the
13
+ # filesystem, so the pairing contract is exercised with plain fixtures, no
14
+ # Rails, no fork.
13
15
  module Pairing
14
16
  module_function
15
17
 
16
18
  # Expand each positional source: a directory -> its sorted **/*.rb files
17
- # (relative to project_root); a file (or glob, or anything non-directory) ->
18
- # itself. Flattened, deduped, order-stable.
19
+ # (relative to project_root); a file (or glob, or anything non-directory)
20
+ # -> itself. Flattened, deduped, order-stable.
21
+ #
22
+ # @param args [Array<String>] source paths or directories.
23
+ # @param project_root [String] repository root for relative expansion.
24
+ # @return [Array<String>] flattened, deduped source file list.
19
25
  def expand_sources(args, project_root:)
20
26
  root = File.expand_path(project_root)
21
27
  Array(args).flat_map do |arg|
@@ -30,8 +36,13 @@ module Mutineer
30
36
 
31
37
  # The first EXISTING candidate test path for a source (relative to
32
38
  # project_root), or nil. `prefer` is the resolved framework ("minitest" |
33
- # "rspec"): its candidates are tried first, the other framework's as fallback,
34
- # so a minitest default still finds a spec and vice-versa.
39
+ # "rspec"): its candidates are tried first, the other framework's as
40
+ # fallback, so a minitest default still finds a spec and vice-versa.
41
+ #
42
+ # @param source_rel [String] relative source path.
43
+ # @param project_root [String] repository root for existence checks.
44
+ # @param prefer [String] preferred framework name.
45
+ # @return [String, nil] existing test path or nil.
35
46
  def infer_test(source_rel, project_root:, prefer: "minitest")
36
47
  base, lib = logical_path(source_rel)
37
48
  candidates(base, lib, prefer).find do |rel|
@@ -39,10 +50,13 @@ module Mutineer
39
50
  end
40
51
  end
41
52
 
42
- # Strip the source root to a logical path (no ".rb") and flag lib/ sources.
43
- # app/foo/bar.rb and lib/foo/bar.rb both -> "foo/bar"; anything else -> the
44
- # path minus ".rb" (still attempted). Namespaced subdirs are preserved
45
- # verbatim — structural, never constant resolution.
53
+ # Strip the source root to a logical path (no ".rb") and flag lib/
54
+ # sources. app/foo/bar.rb and lib/foo/bar.rb both -> "foo/bar"; anything
55
+ # else -> the path minus ".rb" (still attempted). Namespaced subdirs are
56
+ # preserved verbatim — structural, never constant resolution.
57
+ #
58
+ # @param source_rel [String] relative source path.
59
+ # @return [Array(String, Boolean)] logical base path and whether it came from lib/.
46
60
  def logical_path(source_rel)
47
61
  no_ext = source_rel.sub(/\.rb\z/, "")
48
62
  if no_ext.start_with?("app/")
@@ -56,6 +70,11 @@ module Mutineer
56
70
 
57
71
  # Ordered candidate test paths. lib/ sources also get test/lib/... and
58
72
  # spec/lib/... (Rails apps put lib tests under either layout).
73
+ #
74
+ # @param base [String] logical source path without extension.
75
+ # @param lib [Boolean] whether the source originated from lib/.
76
+ # @param prefer [String] preferred framework name.
77
+ # @return [Array<String>] candidate test paths in preferred order.
59
78
  def candidates(base, lib, prefer)
60
79
  minitest = ["test/#{base}_test.rb"]
61
80
  minitest << "test/lib/#{base}_test.rb" if lib
@@ -3,22 +3,32 @@
3
3
  require "prism"
4
4
 
5
5
  module Mutineer
6
- # Raised only for I/O failures while reading a source file. Prism syntax
7
- # errors are NOT raised — they are in-band via ParseResult#errors.
6
+ # Raised only for I/O failures while reading a source file.
7
+ #
8
+ # Prism syntax errors are NOT raised — they are in-band via ParseResult#errors.
8
9
  class ParseError < StandardError; end
9
10
 
10
- # Thin boundary around Prism. Both methods return a Prism::ParseResult so all
11
- # callers use result.value (AST root), result.source.source (raw bytes), and
12
- # result.errors uniformly. No wrapping struct.
11
+ # Thin boundary around Prism.
12
+ #
13
+ # Both methods return a Prism::ParseResult so all callers use result.value,
14
+ # result.source.source (raw bytes), and result.errors uniformly. No wrapping
15
+ # struct.
13
16
  class Parser
14
- # Returns Prism::ParseResult. Re-raises I/O failures as Mutineer::ParseError.
17
+ # Parses a file with Prism.
18
+ #
19
+ # @param path [String] source file path.
20
+ # @return [Prism::ParseResult] Prism parse result.
21
+ # @raise [Mutineer::ParseError] when file I/O fails.
15
22
  def self.parse_file(path)
16
23
  Prism.parse_file(path)
17
24
  rescue SystemCallError => e
18
25
  raise ParseError, e.message
19
26
  end
20
27
 
21
- # Returns Prism::ParseResult. Never raises; callers check .errors.empty?.
28
+ # Parses source text with Prism.
29
+ #
30
+ # @param source [String] source text.
31
+ # @return [Prism::ParseResult] Prism parse result.
22
32
  def self.parse_string(source)
23
33
  Prism.parse(source)
24
34
  end