rfmt 0.1.0 → 0.2.1
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 +30 -0
- data/Cargo.lock +1748 -133
- data/README.md +458 -19
- data/exe/rfmt +15 -0
- data/ext/rfmt/Cargo.toml +46 -1
- data/ext/rfmt/extconf.rb +5 -5
- data/ext/rfmt/spec/config_spec.rb +39 -0
- data/ext/rfmt/spec/spec_helper.rb +16 -0
- data/ext/rfmt/src/ast/mod.rs +335 -0
- data/ext/rfmt/src/config/mod.rs +403 -0
- data/ext/rfmt/src/emitter/mod.rs +347 -0
- data/ext/rfmt/src/error/mod.rs +48 -0
- data/ext/rfmt/src/lib.rs +59 -36
- data/ext/rfmt/src/logging/logger.rs +128 -0
- data/ext/rfmt/src/logging/mod.rs +3 -0
- data/ext/rfmt/src/parser/mod.rs +9 -0
- data/ext/rfmt/src/parser/prism_adapter.rs +407 -0
- data/ext/rfmt/src/policy/mod.rs +36 -0
- data/ext/rfmt/src/policy/validation.rs +18 -0
- data/lib/rfmt/cache.rb +120 -0
- data/lib/rfmt/cli.rb +280 -0
- data/lib/rfmt/configuration.rb +95 -0
- data/lib/rfmt/prism_bridge.rb +255 -0
- data/lib/rfmt/prism_node_extractor.rb +81 -0
- data/lib/rfmt/rfmt.so +0 -0
- data/lib/rfmt/version.rb +1 -1
- data/lib/rfmt.rb +156 -5
- metadata +29 -7
- data/lib/rfmt/rfmt.bundle +0 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
use crate::ast::{Comment, CommentPosition, CommentType, FormattingInfo, Location, Node, NodeType};
|
|
2
|
+
use crate::error::{Result, RfmtError};
|
|
3
|
+
use crate::parser::RubyParser;
|
|
4
|
+
use serde::{Deserialize, Serialize};
|
|
5
|
+
use std::collections::HashMap;
|
|
6
|
+
|
|
7
|
+
/// Prism parser adapter
|
|
8
|
+
/// This integrates with Ruby Prism parser via Magnus FFI
|
|
9
|
+
pub struct PrismAdapter;
|
|
10
|
+
|
|
11
|
+
impl PrismAdapter {
|
|
12
|
+
pub fn new() -> Self {
|
|
13
|
+
Self
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// Parse JSON from Ruby's PrismBridge
|
|
17
|
+
fn parse_json(&self, json: &str) -> Result<(PrismNode, Vec<PrismComment>)> {
|
|
18
|
+
// Try to parse as new format with comments first
|
|
19
|
+
if let Ok(wrapper) = serde_json::from_str::<PrismWrapper>(json) {
|
|
20
|
+
return Ok((wrapper.ast, wrapper.comments));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Fall back to old format (single node without comments)
|
|
24
|
+
let node: PrismNode = serde_json::from_str(json)
|
|
25
|
+
.map_err(|e| RfmtError::PrismError(format!("Failed to parse Prism JSON: {}", e)))?;
|
|
26
|
+
Ok((node, Vec::new()))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Convert PrismNode to internal Node representation
|
|
30
|
+
fn convert_node(&self, prism_node: &PrismNode) -> Result<Node> {
|
|
31
|
+
// Convert node type (always succeeds, returns Unknown for unsupported types)
|
|
32
|
+
let node_type = NodeType::from_str(&prism_node.node_type);
|
|
33
|
+
|
|
34
|
+
// Convert location
|
|
35
|
+
let location = Location::new(
|
|
36
|
+
prism_node.location.start_line,
|
|
37
|
+
prism_node.location.start_column,
|
|
38
|
+
prism_node.location.end_line,
|
|
39
|
+
prism_node.location.end_column,
|
|
40
|
+
prism_node.location.start_offset,
|
|
41
|
+
prism_node.location.end_offset,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Convert children recursively
|
|
45
|
+
let children: Result<Vec<Node>> = prism_node
|
|
46
|
+
.children
|
|
47
|
+
.iter()
|
|
48
|
+
.map(|child| self.convert_node(child))
|
|
49
|
+
.collect();
|
|
50
|
+
let children = children?;
|
|
51
|
+
|
|
52
|
+
// Convert comments
|
|
53
|
+
let comments: Vec<Comment> = prism_node
|
|
54
|
+
.comments
|
|
55
|
+
.iter()
|
|
56
|
+
.map(|c| self.convert_comment(c))
|
|
57
|
+
.collect();
|
|
58
|
+
|
|
59
|
+
// Convert formatting info
|
|
60
|
+
let formatting = FormattingInfo {
|
|
61
|
+
indent_level: prism_node.formatting.indent_level,
|
|
62
|
+
needs_blank_line_before: prism_node.formatting.needs_blank_line_before,
|
|
63
|
+
needs_blank_line_after: prism_node.formatting.needs_blank_line_after,
|
|
64
|
+
preserve_newlines: prism_node.formatting.preserve_newlines,
|
|
65
|
+
multiline: prism_node.formatting.multiline,
|
|
66
|
+
original_formatting: prism_node.formatting.original_formatting.clone(),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
Ok(Node {
|
|
70
|
+
node_type,
|
|
71
|
+
location,
|
|
72
|
+
children,
|
|
73
|
+
metadata: prism_node.metadata.clone(),
|
|
74
|
+
comments,
|
|
75
|
+
formatting,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Convert PrismComment to internal Comment
|
|
80
|
+
fn convert_comment(&self, comment: &PrismComment) -> Comment {
|
|
81
|
+
let comment_type = match comment.comment_type.as_str() {
|
|
82
|
+
"line" => CommentType::Line,
|
|
83
|
+
"block" => CommentType::Block,
|
|
84
|
+
_ => CommentType::Line, // default to line comment
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
let position = match comment.position.as_str() {
|
|
88
|
+
"leading" => CommentPosition::Leading,
|
|
89
|
+
"trailing" => CommentPosition::Trailing,
|
|
90
|
+
"inner" => CommentPosition::Inner,
|
|
91
|
+
_ => CommentPosition::Leading, // default to leading
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
Comment {
|
|
95
|
+
text: comment.text.clone(),
|
|
96
|
+
location: Location::new(
|
|
97
|
+
comment.location.start_line,
|
|
98
|
+
comment.location.start_column,
|
|
99
|
+
comment.location.end_line,
|
|
100
|
+
comment.location.end_column,
|
|
101
|
+
comment.location.start_offset,
|
|
102
|
+
comment.location.end_offset,
|
|
103
|
+
),
|
|
104
|
+
comment_type,
|
|
105
|
+
position,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
impl RubyParser for PrismAdapter {
|
|
111
|
+
fn parse(&self, json: &str) -> Result<Node> {
|
|
112
|
+
let (prism_ast, top_level_comments) = self.parse_json(json)?;
|
|
113
|
+
let mut node = self.convert_node(&prism_ast)?;
|
|
114
|
+
|
|
115
|
+
// Attach top-level comments to the root node
|
|
116
|
+
if !top_level_comments.is_empty() {
|
|
117
|
+
node.comments
|
|
118
|
+
.extend(top_level_comments.iter().map(|c| self.convert_comment(c)));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Ok(node)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
impl Default for PrismAdapter {
|
|
126
|
+
fn default() -> Self {
|
|
127
|
+
Self::new()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Wrapper for JSON containing both AST and comments
|
|
132
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
133
|
+
pub struct PrismWrapper {
|
|
134
|
+
pub ast: PrismNode,
|
|
135
|
+
pub comments: Vec<PrismComment>,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// JSON representation of a Prism node from Ruby
|
|
139
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
140
|
+
pub struct PrismNode {
|
|
141
|
+
pub node_type: String,
|
|
142
|
+
pub location: PrismLocation,
|
|
143
|
+
pub children: Vec<PrismNode>,
|
|
144
|
+
pub metadata: HashMap<String, String>,
|
|
145
|
+
pub comments: Vec<PrismComment>,
|
|
146
|
+
pub formatting: PrismFormattingInfo,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
150
|
+
pub struct PrismLocation {
|
|
151
|
+
pub start_line: usize,
|
|
152
|
+
pub start_column: usize,
|
|
153
|
+
pub end_line: usize,
|
|
154
|
+
pub end_column: usize,
|
|
155
|
+
pub start_offset: usize,
|
|
156
|
+
pub end_offset: usize,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
160
|
+
pub struct PrismComment {
|
|
161
|
+
pub text: String,
|
|
162
|
+
pub location: PrismLocation,
|
|
163
|
+
pub comment_type: String,
|
|
164
|
+
pub position: String,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
168
|
+
pub struct PrismFormattingInfo {
|
|
169
|
+
pub indent_level: usize,
|
|
170
|
+
pub needs_blank_line_before: bool,
|
|
171
|
+
pub needs_blank_line_after: bool,
|
|
172
|
+
pub preserve_newlines: bool,
|
|
173
|
+
pub multiline: bool,
|
|
174
|
+
pub original_formatting: Option<String>,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[cfg(test)]
|
|
178
|
+
mod tests {
|
|
179
|
+
use super::*;
|
|
180
|
+
|
|
181
|
+
#[test]
|
|
182
|
+
fn test_parse_simple_program() {
|
|
183
|
+
let adapter = PrismAdapter::new();
|
|
184
|
+
let json = r#"{
|
|
185
|
+
"node_type": "program_node",
|
|
186
|
+
"location": {
|
|
187
|
+
"start_line": 1,
|
|
188
|
+
"start_column": 0,
|
|
189
|
+
"end_line": 1,
|
|
190
|
+
"end_column": 12,
|
|
191
|
+
"start_offset": 0,
|
|
192
|
+
"end_offset": 12
|
|
193
|
+
},
|
|
194
|
+
"children": [],
|
|
195
|
+
"metadata": {},
|
|
196
|
+
"comments": [],
|
|
197
|
+
"formatting": {
|
|
198
|
+
"indent_level": 0,
|
|
199
|
+
"needs_blank_line_before": false,
|
|
200
|
+
"needs_blank_line_after": false,
|
|
201
|
+
"preserve_newlines": false,
|
|
202
|
+
"multiline": false,
|
|
203
|
+
"original_formatting": null
|
|
204
|
+
}
|
|
205
|
+
}"#;
|
|
206
|
+
|
|
207
|
+
let result = adapter.parse(json);
|
|
208
|
+
assert!(result.is_ok());
|
|
209
|
+
|
|
210
|
+
let node = result.unwrap();
|
|
211
|
+
assert_eq!(node.node_type, NodeType::ProgramNode);
|
|
212
|
+
assert_eq!(node.location.start_line, 1);
|
|
213
|
+
assert_eq!(node.location.end_line, 1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[test]
|
|
217
|
+
fn test_parse_with_children() {
|
|
218
|
+
let adapter = PrismAdapter::new();
|
|
219
|
+
let json = r#"{
|
|
220
|
+
"node_type": "program_node",
|
|
221
|
+
"location": {
|
|
222
|
+
"start_line": 1,
|
|
223
|
+
"start_column": 0,
|
|
224
|
+
"end_line": 1,
|
|
225
|
+
"end_column": 14,
|
|
226
|
+
"start_offset": 0,
|
|
227
|
+
"end_offset": 14
|
|
228
|
+
},
|
|
229
|
+
"children": [
|
|
230
|
+
{
|
|
231
|
+
"node_type": "class_node",
|
|
232
|
+
"location": {
|
|
233
|
+
"start_line": 1,
|
|
234
|
+
"start_column": 0,
|
|
235
|
+
"end_line": 1,
|
|
236
|
+
"end_column": 14,
|
|
237
|
+
"start_offset": 0,
|
|
238
|
+
"end_offset": 14
|
|
239
|
+
},
|
|
240
|
+
"children": [],
|
|
241
|
+
"metadata": {"name": "Foo"},
|
|
242
|
+
"comments": [],
|
|
243
|
+
"formatting": {
|
|
244
|
+
"indent_level": 0,
|
|
245
|
+
"needs_blank_line_before": false,
|
|
246
|
+
"needs_blank_line_after": false,
|
|
247
|
+
"preserve_newlines": false,
|
|
248
|
+
"multiline": false,
|
|
249
|
+
"original_formatting": null
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
],
|
|
253
|
+
"metadata": {},
|
|
254
|
+
"comments": [],
|
|
255
|
+
"formatting": {
|
|
256
|
+
"indent_level": 0,
|
|
257
|
+
"needs_blank_line_before": false,
|
|
258
|
+
"needs_blank_line_after": false,
|
|
259
|
+
"preserve_newlines": false,
|
|
260
|
+
"multiline": false,
|
|
261
|
+
"original_formatting": null
|
|
262
|
+
}
|
|
263
|
+
}"#;
|
|
264
|
+
|
|
265
|
+
let result = adapter.parse(json);
|
|
266
|
+
assert!(result.is_ok());
|
|
267
|
+
|
|
268
|
+
let node = result.unwrap();
|
|
269
|
+
assert_eq!(node.node_type, NodeType::ProgramNode);
|
|
270
|
+
assert_eq!(node.children.len(), 1);
|
|
271
|
+
|
|
272
|
+
let child = &node.children[0];
|
|
273
|
+
assert_eq!(child.node_type, NodeType::ClassNode);
|
|
274
|
+
assert_eq!(child.metadata.get("name"), Some(&"Foo".to_string()));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#[test]
|
|
278
|
+
fn test_parse_with_metadata() {
|
|
279
|
+
let adapter = PrismAdapter::new();
|
|
280
|
+
let json = r#"{
|
|
281
|
+
"node_type": "def_node",
|
|
282
|
+
"location": {
|
|
283
|
+
"start_line": 1,
|
|
284
|
+
"start_column": 0,
|
|
285
|
+
"end_line": 1,
|
|
286
|
+
"end_column": 10,
|
|
287
|
+
"start_offset": 0,
|
|
288
|
+
"end_offset": 10
|
|
289
|
+
},
|
|
290
|
+
"children": [],
|
|
291
|
+
"metadata": {
|
|
292
|
+
"name": "hello",
|
|
293
|
+
"parameters_count": "1"
|
|
294
|
+
},
|
|
295
|
+
"comments": [],
|
|
296
|
+
"formatting": {
|
|
297
|
+
"indent_level": 0,
|
|
298
|
+
"needs_blank_line_before": false,
|
|
299
|
+
"needs_blank_line_after": false,
|
|
300
|
+
"preserve_newlines": false,
|
|
301
|
+
"multiline": false,
|
|
302
|
+
"original_formatting": null
|
|
303
|
+
}
|
|
304
|
+
}"#;
|
|
305
|
+
|
|
306
|
+
let result = adapter.parse(json);
|
|
307
|
+
assert!(result.is_ok());
|
|
308
|
+
|
|
309
|
+
let node = result.unwrap();
|
|
310
|
+
assert_eq!(node.node_type, NodeType::DefNode);
|
|
311
|
+
assert_eq!(node.metadata.get("name"), Some(&"hello".to_string()));
|
|
312
|
+
assert_eq!(
|
|
313
|
+
node.metadata.get("parameters_count"),
|
|
314
|
+
Some(&"1".to_string())
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
#[test]
|
|
319
|
+
fn test_parse_multiline() {
|
|
320
|
+
let adapter = PrismAdapter::new();
|
|
321
|
+
let json = r#"{
|
|
322
|
+
"node_type": "class_node",
|
|
323
|
+
"location": {
|
|
324
|
+
"start_line": 1,
|
|
325
|
+
"start_column": 0,
|
|
326
|
+
"end_line": 3,
|
|
327
|
+
"end_column": 3,
|
|
328
|
+
"start_offset": 0,
|
|
329
|
+
"end_offset": 20
|
|
330
|
+
},
|
|
331
|
+
"children": [],
|
|
332
|
+
"metadata": {"name": "Foo"},
|
|
333
|
+
"comments": [],
|
|
334
|
+
"formatting": {
|
|
335
|
+
"indent_level": 0,
|
|
336
|
+
"needs_blank_line_before": false,
|
|
337
|
+
"needs_blank_line_after": false,
|
|
338
|
+
"preserve_newlines": false,
|
|
339
|
+
"multiline": true,
|
|
340
|
+
"original_formatting": null
|
|
341
|
+
}
|
|
342
|
+
}"#;
|
|
343
|
+
|
|
344
|
+
let result = adapter.parse(json);
|
|
345
|
+
assert!(result.is_ok());
|
|
346
|
+
|
|
347
|
+
let node = result.unwrap();
|
|
348
|
+
assert_eq!(node.node_type, NodeType::ClassNode);
|
|
349
|
+
assert_eq!(node.formatting.multiline, true);
|
|
350
|
+
assert!(node.is_multiline());
|
|
351
|
+
assert_eq!(node.line_count(), 3);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#[test]
|
|
355
|
+
fn test_parse_invalid_json() {
|
|
356
|
+
let adapter = PrismAdapter::new();
|
|
357
|
+
let json = "invalid json";
|
|
358
|
+
|
|
359
|
+
let result = adapter.parse(json);
|
|
360
|
+
assert!(result.is_err());
|
|
361
|
+
|
|
362
|
+
match result {
|
|
363
|
+
Err(RfmtError::PrismError(msg)) => {
|
|
364
|
+
assert!(msg.contains("Failed to parse Prism JSON"));
|
|
365
|
+
}
|
|
366
|
+
_ => panic!("Expected PrismError"),
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
#[test]
|
|
371
|
+
fn test_parse_unknown_node_type() {
|
|
372
|
+
let adapter = PrismAdapter::new();
|
|
373
|
+
let json = r#"{
|
|
374
|
+
"node_type": "totally_unknown_node",
|
|
375
|
+
"location": {
|
|
376
|
+
"start_line": 1,
|
|
377
|
+
"start_column": 0,
|
|
378
|
+
"end_line": 1,
|
|
379
|
+
"end_column": 10,
|
|
380
|
+
"start_offset": 0,
|
|
381
|
+
"end_offset": 10
|
|
382
|
+
},
|
|
383
|
+
"children": [],
|
|
384
|
+
"metadata": {},
|
|
385
|
+
"comments": [],
|
|
386
|
+
"formatting": {
|
|
387
|
+
"indent_level": 0,
|
|
388
|
+
"needs_blank_line_before": false,
|
|
389
|
+
"needs_blank_line_after": false,
|
|
390
|
+
"preserve_newlines": false,
|
|
391
|
+
"multiline": false,
|
|
392
|
+
"original_formatting": null
|
|
393
|
+
}
|
|
394
|
+
}"#;
|
|
395
|
+
|
|
396
|
+
let result = adapter.parse(json);
|
|
397
|
+
assert!(result.is_ok());
|
|
398
|
+
|
|
399
|
+
let node = result.unwrap();
|
|
400
|
+
assert_eq!(
|
|
401
|
+
node.node_type,
|
|
402
|
+
NodeType::Unknown("totally_unknown_node".to_string())
|
|
403
|
+
);
|
|
404
|
+
assert!(node.is_unknown());
|
|
405
|
+
assert_eq!(node.unknown_type(), Some("totally_unknown_node"));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
pub mod validation;
|
|
2
|
+
|
|
3
|
+
use crate::error::Result;
|
|
4
|
+
|
|
5
|
+
/// Security policy for rfmt operations
|
|
6
|
+
#[derive(Debug, Clone)]
|
|
7
|
+
pub struct SecurityPolicy {
|
|
8
|
+
/// Maximum file size in bytes (default: 10MB)
|
|
9
|
+
pub max_file_size: u64,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
impl SecurityPolicy {
|
|
13
|
+
/// Validate source code size
|
|
14
|
+
pub fn validate_source_size(&self, source: &str) -> Result<()> {
|
|
15
|
+
validation::validate_source_size(source, self.max_file_size)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
impl Default for SecurityPolicy {
|
|
20
|
+
fn default() -> Self {
|
|
21
|
+
Self {
|
|
22
|
+
max_file_size: 10 * 1024 * 1024, // 10MB
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[cfg(test)]
|
|
28
|
+
mod tests {
|
|
29
|
+
use super::*;
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn test_default_policy() {
|
|
33
|
+
let policy = SecurityPolicy::default();
|
|
34
|
+
assert_eq!(policy.max_file_size, 10 * 1024 * 1024);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
use crate::error::{Result, RfmtError};
|
|
2
|
+
|
|
3
|
+
/// Validate source code size
|
|
4
|
+
pub fn validate_source_size(source: &str, max_size: u64) -> Result<()> {
|
|
5
|
+
let size = source.len() as u64;
|
|
6
|
+
|
|
7
|
+
if size > max_size {
|
|
8
|
+
return Err(RfmtError::UnsupportedFeature {
|
|
9
|
+
feature: "Large file".to_string(),
|
|
10
|
+
explanation: format!(
|
|
11
|
+
"Source code is too large ({} bytes, max {} bytes)",
|
|
12
|
+
size, max_size
|
|
13
|
+
),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Ok(())
|
|
18
|
+
}
|
data/lib/rfmt/cache.rb
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
module Rfmt
|
|
8
|
+
# Cache system for formatted files
|
|
9
|
+
# Uses SHA256 hash of file content to determine if formatting is needed
|
|
10
|
+
class Cache
|
|
11
|
+
class CacheError < StandardError; end
|
|
12
|
+
|
|
13
|
+
DEFAULT_CACHE_DIR = File.expand_path('~/.cache/rfmt').freeze
|
|
14
|
+
CACHE_VERSION = '1'
|
|
15
|
+
|
|
16
|
+
attr_reader :cache_dir
|
|
17
|
+
|
|
18
|
+
def initialize(cache_dir: DEFAULT_CACHE_DIR)
|
|
19
|
+
@cache_dir = cache_dir
|
|
20
|
+
@cache_data = {}
|
|
21
|
+
ensure_cache_dir
|
|
22
|
+
load_cache
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Check if file needs formatting
|
|
26
|
+
# Returns true if file content has changed or not in cache
|
|
27
|
+
def needs_formatting?(file_path)
|
|
28
|
+
return true unless File.exist?(file_path)
|
|
29
|
+
|
|
30
|
+
current_hash = file_hash(file_path)
|
|
31
|
+
cached_hash = @cache_data.dig(file_path, 'hash')
|
|
32
|
+
|
|
33
|
+
current_hash != cached_hash
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Mark file as formatted with current content
|
|
37
|
+
def mark_formatted(file_path)
|
|
38
|
+
return unless File.exist?(file_path)
|
|
39
|
+
|
|
40
|
+
@cache_data[file_path] = {
|
|
41
|
+
'hash' => file_hash(file_path),
|
|
42
|
+
'formatted_at' => Time.now.to_i,
|
|
43
|
+
'version' => CACHE_VERSION
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Save cache to disk
|
|
48
|
+
def save
|
|
49
|
+
cache_file = File.join(@cache_dir, 'cache.json')
|
|
50
|
+
File.write(cache_file, JSON.pretty_generate(@cache_data))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Clear all cache data
|
|
54
|
+
def clear
|
|
55
|
+
@cache_data = {}
|
|
56
|
+
save
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Remove cache for specific file
|
|
60
|
+
def invalidate(file_path)
|
|
61
|
+
@cache_data.delete(file_path)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get cache statistics
|
|
65
|
+
def stats
|
|
66
|
+
{
|
|
67
|
+
total_files: @cache_data.size,
|
|
68
|
+
cache_dir: @cache_dir,
|
|
69
|
+
cache_size_bytes: cache_size
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Prune old cache entries (files that no longer exist)
|
|
74
|
+
def prune
|
|
75
|
+
before_count = @cache_data.size
|
|
76
|
+
@cache_data.delete_if { |file_path, _| !File.exist?(file_path) }
|
|
77
|
+
after_count = @cache_data.size
|
|
78
|
+
pruned = before_count - after_count
|
|
79
|
+
|
|
80
|
+
save if pruned.positive?
|
|
81
|
+
pruned
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def ensure_cache_dir
|
|
87
|
+
FileUtils.mkdir_p(@cache_dir)
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
raise CacheError, "Failed to create cache directory: #{e.message}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def load_cache
|
|
93
|
+
cache_file = File.join(@cache_dir, 'cache.json')
|
|
94
|
+
return unless File.exist?(cache_file)
|
|
95
|
+
|
|
96
|
+
content = File.read(cache_file)
|
|
97
|
+
@cache_data = JSON.parse(content)
|
|
98
|
+
rescue JSON::ParserError => e
|
|
99
|
+
warn "Warning: Failed to parse cache file, starting with empty cache: #{e.message}"
|
|
100
|
+
@cache_data = {}
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
warn "Warning: Failed to load cache, starting with empty cache: #{e.message}"
|
|
103
|
+
@cache_data = {}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def file_hash(file_path)
|
|
107
|
+
content = File.read(file_path)
|
|
108
|
+
Digest::SHA256.hexdigest(content)
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
raise CacheError, "Failed to read file #{file_path}: #{e.message}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def cache_size
|
|
114
|
+
cache_file = File.join(@cache_dir, 'cache.json')
|
|
115
|
+
return 0 unless File.exist?(cache_file)
|
|
116
|
+
|
|
117
|
+
File.size(cache_file)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|