rfmt 1.7.0 → 2.0.0.beta1
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 +4 -4
- data/CHANGELOG.md +25 -0
- data/Cargo.lock +90 -1225
- data/Cargo.toml +6 -0
- data/README.md +32 -28
- data/ext/rfmt/Cargo.toml +9 -28
- data/ext/rfmt/src/ast/mod.rs +2 -0
- data/ext/rfmt/src/config/mod.rs +267 -30
- data/ext/rfmt/src/error/mod.rs +14 -3
- data/ext/rfmt/src/format/formatter.rs +22 -11
- data/ext/rfmt/src/format/registry.rs +8 -0
- data/ext/rfmt/src/format/rule.rs +208 -136
- data/ext/rfmt/src/format/rules/call.rs +14 -22
- data/ext/rfmt/src/format/rules/fallback.rs +4 -11
- data/ext/rfmt/src/format/rules/if_unless.rs +25 -3
- data/ext/rfmt/src/format/rules/loops.rs +3 -1
- data/ext/rfmt/src/format/rules/variable_write.rs +16 -9
- data/ext/rfmt/src/lib.rs +45 -12
- data/ext/rfmt/src/parser/mod.rs +2 -0
- data/ext/rfmt/src/parser/native_adapter.rs +2735 -0
- data/ext/rfmt/src/parser/prism_adapter.rs +5 -0
- data/ext/rfmt/src/validation.rs +69 -0
- data/ext/rfmt/tests/fixtures/parity/comments_mixed.rb +9 -0
- data/ext/rfmt/tests/fixtures/parity/constructs.rb +50 -0
- data/ext/rfmt/tests/fixtures/parity/embdoc.rb +12 -0
- data/ext/rfmt/tests/fixtures/parity/heredoc_assign.rb +15 -0
- data/ext/rfmt/tests/fixtures/parity/heredoc_call_args.rb +17 -0
- data/ext/rfmt/tests/fixtures/parity/metadata_classes.rb +30 -0
- data/ext/rfmt/tests/fixtures/parity/metadata_conditionals.rb +14 -0
- data/ext/rfmt/tests/fixtures/parity/metadata_defs.rb +33 -0
- data/ext/rfmt/tests/fixtures/parity/multibyte.rb +14 -0
- data/ext/rfmt/tests/fixtures/parity/numeric.rb +6 -0
- data/ext/rfmt/tests/fixtures/parity/plain.rb +31 -0
- data/ext/rfmt/tests/native_parity.rs +245 -0
- data/ext/rfmt/tests/ruby_prism_smoke.rs +66 -0
- data/lib/rfmt/cli.rb +37 -7
- data/lib/rfmt/configuration.rb +2 -20
- data/lib/rfmt/version.rb +1 -1
- data/lib/rfmt.rb +47 -19
- metadata +17 -18
- data/lib/rfmt/prism_bridge.rb +0 -529
- data/lib/rfmt/prism_node_extractor.rb +0 -115
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
//! Test support only: no production caller since the phase 7 legacy-path
|
|
2
|
+
//! deletion. Deserializes the frozen golden JSON fixtures under
|
|
3
|
+
//! tests/fixtures/parity/ so native_parity.rs can pin NativeAdapter's output
|
|
4
|
+
//! against future ruby-prism crate bumps.
|
|
5
|
+
|
|
1
6
|
use crate::ast::{Comment, CommentPosition, CommentType, FormattingInfo, Location, Node, NodeType};
|
|
2
7
|
use crate::error::{Result, RfmtError};
|
|
3
8
|
use crate::parser::RubyParser;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//! Output guard: never return syntactically invalid Ruby to the caller.
|
|
2
|
+
//! Moved here from lib/rfmt.rb's validate_output! at the phase-6 switchover.
|
|
3
|
+
|
|
4
|
+
use crate::error::{Result, RfmtError};
|
|
5
|
+
|
|
6
|
+
pub fn validate_output(formatted: &str) -> Result<()> {
|
|
7
|
+
let bytes = formatted.as_bytes();
|
|
8
|
+
let parse_result = ruby_prism::parse(bytes);
|
|
9
|
+
|
|
10
|
+
let Some(error) = parse_result.errors().next() else {
|
|
11
|
+
return Ok(());
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
let line = 1 + bytes[..error.location().start_offset()]
|
|
15
|
+
.iter()
|
|
16
|
+
.filter(|&&b| b == b'\n')
|
|
17
|
+
.count();
|
|
18
|
+
Err(RfmtError::ValidationError(format!(
|
|
19
|
+
"Formatter produced syntactically invalid output (this is a bug in rfmt, not in your code): {} at line {}",
|
|
20
|
+
error.message(),
|
|
21
|
+
line
|
|
22
|
+
)))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[cfg(test)]
|
|
26
|
+
mod tests {
|
|
27
|
+
use super::*;
|
|
28
|
+
|
|
29
|
+
#[test]
|
|
30
|
+
fn accepts_valid_ruby() {
|
|
31
|
+
assert!(validate_output("x = 1\n").is_ok());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[test]
|
|
35
|
+
fn rejects_invalid_ruby_with_validation_error() {
|
|
36
|
+
let err = validate_output("def broken(\n").unwrap_err();
|
|
37
|
+
|
|
38
|
+
match err {
|
|
39
|
+
RfmtError::ValidationError(message) => {
|
|
40
|
+
assert!(
|
|
41
|
+
message.starts_with(
|
|
42
|
+
"Formatter produced syntactically invalid output (this is a bug in rfmt, not in your code): "
|
|
43
|
+
),
|
|
44
|
+
"unexpected message: {message}"
|
|
45
|
+
);
|
|
46
|
+
assert!(
|
|
47
|
+
message.contains(" at line 1"),
|
|
48
|
+
"unexpected message: {message}"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
other => panic!("expected ValidationError, got {other:?}"),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[test]
|
|
56
|
+
fn reports_the_line_of_the_first_error() {
|
|
57
|
+
let err = validate_output("x = 1\ny = 2\ndef broken(\n").unwrap_err();
|
|
58
|
+
|
|
59
|
+
match err {
|
|
60
|
+
RfmtError::ValidationError(message) => {
|
|
61
|
+
assert!(
|
|
62
|
+
message.contains(" at line 3"),
|
|
63
|
+
"unexpected message: {message}"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
other => panic!("expected ValidationError, got {other:?}"),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
expected = :ready
|
|
2
|
+
|
|
3
|
+
case payload
|
|
4
|
+
in [Integer => first, *rest]
|
|
5
|
+
first + rest.sum
|
|
6
|
+
in { status: String => status, code: 200 | 201 }
|
|
7
|
+
status
|
|
8
|
+
in ^expected
|
|
9
|
+
:pinned
|
|
10
|
+
else
|
|
11
|
+
:unmatched
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
risky!
|
|
16
|
+
rescue KeyError => e
|
|
17
|
+
recover(e)
|
|
18
|
+
rescue StandardError
|
|
19
|
+
retry_count += 1
|
|
20
|
+
retry if retry_count < 3
|
|
21
|
+
else
|
|
22
|
+
celebrate
|
|
23
|
+
ensure
|
|
24
|
+
cleanup
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
counters = Hash.new(0)
|
|
28
|
+
counters[:hits] ||= 0
|
|
29
|
+
counters[:hits] += 1
|
|
30
|
+
@cache &&= @cache.compact
|
|
31
|
+
$verbose ||= false
|
|
32
|
+
CONFIG = { a: 3r, b: 3.14r, c: 2i, d: 2ri }
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
def singleton_helper = :ok
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
alias aka singleton_helper
|
|
39
|
+
alias $err $stderr
|
|
40
|
+
undef :aka
|
|
41
|
+
|
|
42
|
+
flip = items.select { |l| (l == first)..(l == last) }
|
|
43
|
+
|
|
44
|
+
lines = ->(input) { input.each_line.to_a }
|
|
45
|
+
x, *middle, z = lines.call(<<~TXT)
|
|
46
|
+
alpha
|
|
47
|
+
beta
|
|
48
|
+
gamma
|
|
49
|
+
TXT
|
|
50
|
+
puts x, middle, z
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class Foo::Bar < Baz::Qux
|
|
2
|
+
def change
|
|
3
|
+
x = compute(1)
|
|
4
|
+
@count = x
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class AddUsers < ActiveRecord::Migration[8.1]
|
|
9
|
+
def change
|
|
10
|
+
create_table :users
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Simple < Base
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Plain
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module Alpha::Beta
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module Gamma
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class ::TopScoped < ::Deep::Nested::Base
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
module Alpha::Beta::Gamma
|
|
30
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
def !@
|
|
2
|
+
true
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
def +@
|
|
6
|
+
self
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def []=(key, value)
|
|
10
|
+
store(key, value)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.build(name)
|
|
14
|
+
new(name)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
obj = Object.new
|
|
18
|
+
|
|
19
|
+
def obj.run
|
|
20
|
+
1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def bare arg
|
|
24
|
+
arg
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def none
|
|
28
|
+
0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def empty_parens()
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class Greeter
|
|
2
|
+
attr_reader :name
|
|
3
|
+
|
|
4
|
+
def initialize(name)
|
|
5
|
+
@name = name
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def greet(loud: false)
|
|
9
|
+
message = "hello, #{name}"
|
|
10
|
+
if loud
|
|
11
|
+
message.upcase
|
|
12
|
+
else
|
|
13
|
+
message
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module Registry
|
|
19
|
+
DEFAULT = Greeter.new("world")
|
|
20
|
+
|
|
21
|
+
def self.lookup(key)
|
|
22
|
+
entries.fetch(key) { DEFAULT }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
items = [1, 2.5, :three, nil, true]
|
|
27
|
+
totals = { "sum" => items.compact.size, count: items.length }
|
|
28
|
+
items.each_with_index do |item, index|
|
|
29
|
+
puts "#{index}: #{item}" unless item.nil?
|
|
30
|
+
end
|
|
31
|
+
result = Registry.lookup(:default)&.greet(loud: totals[:count] > 3)
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
//! Parity tests for the native ruby-prism converter.
|
|
2
|
+
//!
|
|
3
|
+
//! Each fixture under tests/fixtures/parity/ exists as a .rb source and the
|
|
4
|
+
//! frozen golden JSON the (now deleted) Ruby PrismBridge produced for it.
|
|
5
|
+
//! The .rb goes through NativeAdapter, the .json through the test-support
|
|
6
|
+
//! PrismAdapter, and the trees must agree on node types, all six location
|
|
7
|
+
//! fields, children, formatting.multiline, metadata, and comments. This pins
|
|
8
|
+
//! NativeAdapter's output shape against future ruby-prism crate bumps; only
|
|
9
|
+
//! `bundle exec ruby scripts/gen_parity_fixtures.rb` may regenerate the JSON.
|
|
10
|
+
|
|
11
|
+
use rfmt::ast::{Location, Node};
|
|
12
|
+
use rfmt::parser::{NativeAdapter, PrismAdapter, RubyParser};
|
|
13
|
+
use std::collections::HashMap;
|
|
14
|
+
use std::fs;
|
|
15
|
+
use std::path::{Path, PathBuf};
|
|
16
|
+
|
|
17
|
+
/// Keys the bridge still emits but nothing downstream reads; the native
|
|
18
|
+
/// converter intentionally drops them, so they are stripped from the JSON
|
|
19
|
+
/// side before the exact metadata comparison.
|
|
20
|
+
const DEAD_METADATA_KEYS: [&str; 4] = ["parameters_count", "message", "content", "value"];
|
|
21
|
+
|
|
22
|
+
fn fixtures_dir() -> PathBuf {
|
|
23
|
+
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/parity")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn push_diff<T: std::fmt::Debug + PartialEq>(
|
|
27
|
+
diffs: &mut Vec<String>,
|
|
28
|
+
path: &str,
|
|
29
|
+
field: &str,
|
|
30
|
+
json: &T,
|
|
31
|
+
native: &T,
|
|
32
|
+
) {
|
|
33
|
+
if json != native {
|
|
34
|
+
diffs.push(format!(
|
|
35
|
+
"{}.{}: json={:?} native={:?}",
|
|
36
|
+
path, field, json, native
|
|
37
|
+
));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn live_metadata(metadata: &HashMap<String, String>) -> HashMap<String, String> {
|
|
42
|
+
metadata
|
|
43
|
+
.iter()
|
|
44
|
+
.filter(|(key, _)| !DEAD_METADATA_KEYS.contains(&key.as_str()))
|
|
45
|
+
.map(|(key, value)| (key.clone(), value.clone()))
|
|
46
|
+
.collect()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn compare_locations(path: &str, json: &Location, native: &Location, diffs: &mut Vec<String>) {
|
|
50
|
+
push_diff(
|
|
51
|
+
diffs,
|
|
52
|
+
path,
|
|
53
|
+
"start_line",
|
|
54
|
+
&json.start_line,
|
|
55
|
+
&native.start_line,
|
|
56
|
+
);
|
|
57
|
+
push_diff(
|
|
58
|
+
diffs,
|
|
59
|
+
path,
|
|
60
|
+
"start_column",
|
|
61
|
+
&json.start_column,
|
|
62
|
+
&native.start_column,
|
|
63
|
+
);
|
|
64
|
+
push_diff(diffs, path, "end_line", &json.end_line, &native.end_line);
|
|
65
|
+
push_diff(
|
|
66
|
+
diffs,
|
|
67
|
+
path,
|
|
68
|
+
"end_column",
|
|
69
|
+
&json.end_column,
|
|
70
|
+
&native.end_column,
|
|
71
|
+
);
|
|
72
|
+
push_diff(
|
|
73
|
+
diffs,
|
|
74
|
+
path,
|
|
75
|
+
"start_offset",
|
|
76
|
+
&json.start_offset,
|
|
77
|
+
&native.start_offset,
|
|
78
|
+
);
|
|
79
|
+
push_diff(
|
|
80
|
+
diffs,
|
|
81
|
+
path,
|
|
82
|
+
"end_offset",
|
|
83
|
+
&json.end_offset,
|
|
84
|
+
&native.end_offset,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fn compare_nodes(path: &str, json: &Node, native: &Node, diffs: &mut Vec<String>) {
|
|
89
|
+
push_diff(diffs, path, "node_type", &json.node_type, &native.node_type);
|
|
90
|
+
|
|
91
|
+
compare_locations(
|
|
92
|
+
&format!("{}.location", path),
|
|
93
|
+
&json.location,
|
|
94
|
+
&native.location,
|
|
95
|
+
diffs,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
push_diff(
|
|
99
|
+
diffs,
|
|
100
|
+
path,
|
|
101
|
+
"formatting.multiline",
|
|
102
|
+
&json.formatting.multiline,
|
|
103
|
+
&native.formatting.multiline,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
let json_metadata = live_metadata(&json.metadata);
|
|
107
|
+
push_diff(diffs, path, "metadata", &json_metadata, &native.metadata);
|
|
108
|
+
|
|
109
|
+
push_diff(
|
|
110
|
+
diffs,
|
|
111
|
+
path,
|
|
112
|
+
"comments.len",
|
|
113
|
+
&json.comments.len(),
|
|
114
|
+
&native.comments.len(),
|
|
115
|
+
);
|
|
116
|
+
for (i, (jc, nc)) in json.comments.iter().zip(native.comments.iter()).enumerate() {
|
|
117
|
+
let cpath = format!("{}.comments[{}]", path, i);
|
|
118
|
+
push_diff(diffs, &cpath, "text", &jc.text, &nc.text);
|
|
119
|
+
push_diff(
|
|
120
|
+
diffs,
|
|
121
|
+
&cpath,
|
|
122
|
+
"comment_type",
|
|
123
|
+
&jc.comment_type,
|
|
124
|
+
&nc.comment_type,
|
|
125
|
+
);
|
|
126
|
+
push_diff(diffs, &cpath, "position", &jc.position, &nc.position);
|
|
127
|
+
compare_locations(
|
|
128
|
+
&format!("{}.location", cpath),
|
|
129
|
+
&jc.location,
|
|
130
|
+
&nc.location,
|
|
131
|
+
diffs,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
push_diff(
|
|
136
|
+
diffs,
|
|
137
|
+
path,
|
|
138
|
+
"children.len",
|
|
139
|
+
&json.children.len(),
|
|
140
|
+
&native.children.len(),
|
|
141
|
+
);
|
|
142
|
+
for (i, (jc, nc)) in json.children.iter().zip(native.children.iter()).enumerate() {
|
|
143
|
+
compare_nodes(&format!("{}.children[{}]", path, i), jc, nc, diffs);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[test]
|
|
148
|
+
fn native_conversion_matches_ruby_bridge() {
|
|
149
|
+
let dir = fixtures_dir();
|
|
150
|
+
let mut names: Vec<PathBuf> = fs::read_dir(&dir)
|
|
151
|
+
.expect("fixtures dir missing; add fixtures under tests/fixtures/parity")
|
|
152
|
+
.map(|e| e.unwrap().path())
|
|
153
|
+
.filter(|p| p.extension().is_some_and(|ext| ext == "rb"))
|
|
154
|
+
.collect();
|
|
155
|
+
names.sort();
|
|
156
|
+
assert!(
|
|
157
|
+
names.len() >= 11,
|
|
158
|
+
"expected the full parity fixture set, found {:?}",
|
|
159
|
+
names
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
let mut failures = Vec::new();
|
|
163
|
+
for rb_path in &names {
|
|
164
|
+
let name = rb_path.file_stem().unwrap().to_string_lossy().into_owned();
|
|
165
|
+
let source = fs::read_to_string(rb_path).unwrap();
|
|
166
|
+
let json = fs::read_to_string(rb_path.with_extension("json")).unwrap_or_else(|_| {
|
|
167
|
+
panic!(
|
|
168
|
+
"{}.json missing; run `bundle exec ruby scripts/gen_parity_fixtures.rb`",
|
|
169
|
+
name
|
|
170
|
+
)
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
let json_tree = PrismAdapter::new().parse(&json).expect("bridge JSON parse");
|
|
174
|
+
let native_tree = NativeAdapter::new().parse(&source).expect("native parse");
|
|
175
|
+
|
|
176
|
+
let mut diffs = Vec::new();
|
|
177
|
+
compare_nodes("root", &json_tree, &native_tree, &mut diffs);
|
|
178
|
+
if !diffs.is_empty() {
|
|
179
|
+
failures.push(format!("[{}]\n{}", name, diffs.join("\n")));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
assert!(failures.is_empty(), "\n{}", failures.join("\n\n"));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#[test]
|
|
187
|
+
fn comparator_detects_a_mutated_tree() {
|
|
188
|
+
let source = fs::read_to_string(fixtures_dir().join("plain.rb")).unwrap();
|
|
189
|
+
let reference = NativeAdapter::new().parse(&source).unwrap();
|
|
190
|
+
let mut mutated = reference.clone();
|
|
191
|
+
let first = mutated.children.first_mut().expect("fixture has children");
|
|
192
|
+
first.location.end_line += 1;
|
|
193
|
+
|
|
194
|
+
let mut diffs = Vec::new();
|
|
195
|
+
compare_nodes("root", &reference, &mutated, &mut diffs);
|
|
196
|
+
assert_eq!(diffs.len(), 1, "{diffs:?}");
|
|
197
|
+
assert!(
|
|
198
|
+
diffs[0].starts_with("root.children[0].location.end_line: "),
|
|
199
|
+
"{diffs:?}"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[test]
|
|
204
|
+
fn comparator_detects_a_mutated_metadata_value() {
|
|
205
|
+
let reference = NativeAdapter::new()
|
|
206
|
+
.parse("def foo(a)\n a\nend\n")
|
|
207
|
+
.unwrap();
|
|
208
|
+
let mut mutated = reference.clone();
|
|
209
|
+
let def = mutated.children.first_mut().expect("def child");
|
|
210
|
+
assert_eq!(def.metadata.get("name").map(String::as_str), Some("foo"));
|
|
211
|
+
def.metadata.insert("name".to_string(), "bar".to_string());
|
|
212
|
+
|
|
213
|
+
let mut diffs = Vec::new();
|
|
214
|
+
compare_nodes("root", &reference, &mutated, &mut diffs);
|
|
215
|
+
assert_eq!(diffs.len(), 1, "{diffs:?}");
|
|
216
|
+
assert!(
|
|
217
|
+
diffs[0].starts_with("root.children[0].metadata: "),
|
|
218
|
+
"{diffs:?}"
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[test]
|
|
223
|
+
fn comparator_detects_a_mutated_comment() {
|
|
224
|
+
let reference = NativeAdapter::new().parse("# hello\nx = 1\n").unwrap();
|
|
225
|
+
let mut mutated = reference.clone();
|
|
226
|
+
let comment = mutated.comments.first_mut().expect("root comment");
|
|
227
|
+
assert_eq!(comment.text, "# hello");
|
|
228
|
+
comment.text = "# tampered".to_string();
|
|
229
|
+
|
|
230
|
+
let mut diffs = Vec::new();
|
|
231
|
+
compare_nodes("root", &reference, &mutated, &mut diffs);
|
|
232
|
+
assert_eq!(diffs.len(), 1, "{diffs:?}");
|
|
233
|
+
assert!(diffs[0].starts_with("root.comments[0].text: "), "{diffs:?}");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#[test]
|
|
237
|
+
fn native_adapter_reports_parse_errors_with_position() {
|
|
238
|
+
let err = NativeAdapter::new()
|
|
239
|
+
.parse("x = 1\ndef broken(")
|
|
240
|
+
.unwrap_err();
|
|
241
|
+
let message = err.to_string();
|
|
242
|
+
assert!(message.contains("Parse error"), "{}", message);
|
|
243
|
+
// Same line:column shape the Ruby bridge raises (line 2, byte column)
|
|
244
|
+
assert!(message.contains("2:"), "{}", message);
|
|
245
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
//! Spike gate for the ruby-prism migration (#110 Phase 3): proves the crate
|
|
2
|
+
//! parses with the byte-offset and comment semantics the formatter relies on.
|
|
3
|
+
|
|
4
|
+
#[test]
|
|
5
|
+
fn parses_heredoc_comments_and_safe_navigation() {
|
|
6
|
+
let source = r#"result = fetch&.body
|
|
7
|
+
sql = <<~SQL
|
|
8
|
+
SELECT 1
|
|
9
|
+
SQL
|
|
10
|
+
# leading comment
|
|
11
|
+
value = compute(sql) # trailing comment
|
|
12
|
+
"#;
|
|
13
|
+
|
|
14
|
+
let result = ruby_prism::parse(source.as_bytes());
|
|
15
|
+
|
|
16
|
+
assert!(result.errors().next().is_none());
|
|
17
|
+
assert_eq!(result.comments().count(), 2);
|
|
18
|
+
|
|
19
|
+
let comment_offsets: Vec<(usize, usize)> = result
|
|
20
|
+
.comments()
|
|
21
|
+
.map(|c| {
|
|
22
|
+
let loc = c.location();
|
|
23
|
+
(loc.start_offset(), loc.end_offset())
|
|
24
|
+
})
|
|
25
|
+
.collect();
|
|
26
|
+
let leading = &source[comment_offsets[0].0..comment_offsets[0].1];
|
|
27
|
+
let trailing = &source[comment_offsets[1].0..comment_offsets[1].1];
|
|
28
|
+
assert_eq!(leading, "# leading comment");
|
|
29
|
+
assert_eq!(trailing, "# trailing comment");
|
|
30
|
+
|
|
31
|
+
let program = result.node();
|
|
32
|
+
let program = program.as_program_node().expect("program node");
|
|
33
|
+
let statements = program.statements();
|
|
34
|
+
assert_eq!(statements.body().iter().count(), 3);
|
|
35
|
+
|
|
36
|
+
let first = statements.body().iter().next().expect("first statement");
|
|
37
|
+
let loc = first.location();
|
|
38
|
+
assert_eq!(
|
|
39
|
+
&source[loc.start_offset()..loc.end_offset()],
|
|
40
|
+
"result = fetch&.body"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[test]
|
|
45
|
+
fn reports_errors_for_invalid_source() {
|
|
46
|
+
let result = ruby_prism::parse(b"def broken(");
|
|
47
|
+
assert!(result.errors().next().is_some());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[test]
|
|
51
|
+
fn multibyte_source_keeps_byte_offsets() {
|
|
52
|
+
let source = "x = \"あいう\"\ny = 1\n";
|
|
53
|
+
let result = ruby_prism::parse(source.as_bytes());
|
|
54
|
+
assert!(result.errors().next().is_none());
|
|
55
|
+
|
|
56
|
+
let program = result.node();
|
|
57
|
+
let program = program.as_program_node().expect("program node");
|
|
58
|
+
let second = program
|
|
59
|
+
.statements()
|
|
60
|
+
.body()
|
|
61
|
+
.iter()
|
|
62
|
+
.nth(1)
|
|
63
|
+
.expect("second statement");
|
|
64
|
+
let loc = second.location();
|
|
65
|
+
assert_eq!(&source[loc.start_offset()..loc.end_offset()], "y = 1");
|
|
66
|
+
}
|