darklua_core/rules/
append_text_comment.rs1use std::fs;
2use std::path::{Path, PathBuf};
3use std::sync::OnceLock;
4
5use crate::nodes::{Block, Token, TriviaKind};
6use crate::rules::{
7 verify_property_collisions, verify_required_any_properties, Context, Rule, RuleConfiguration,
8 RuleConfigurationError, RuleProcessResult, RuleProperties,
9};
10
11use super::{FlawlessRule, ShiftTokenLine};
12
13pub const APPEND_TEXT_COMMENT_RULE_NAME: &str = "append_text_comment";
14
15#[derive(Debug, Default)]
17pub struct AppendTextComment {
18 text_value: OnceLock<Result<String, String>>,
19 text_content: TextContent,
20 location: AppendLocation,
21}
22
23impl AppendTextComment {
24 pub fn new(value: impl Into<String>) -> Self {
25 Self {
26 text_value: Default::default(),
27 text_content: TextContent::Value(value.into()),
28 location: Default::default(),
29 }
30 }
31
32 pub fn from_file_content(file_path: impl Into<PathBuf>) -> Self {
33 Self {
34 text_value: Default::default(),
35 text_content: TextContent::FilePath(file_path.into()),
36 location: Default::default(),
37 }
38 }
39
40 pub fn at_end(mut self) -> Self {
41 self.location = AppendLocation::End;
42 self
43 }
44
45 fn text(&self, project_path: &Path) -> Result<String, String> {
46 self.text_value
47 .get_or_init(|| {
48 match &self.text_content {
49 TextContent::None => Err("".to_owned()),
50 TextContent::Value(value) => Ok(value.clone()),
51 TextContent::FilePath(file_path) => {
52 fs::read_to_string(project_path.join(file_path)).map_err(|err| {
53 format!("unable to read file `{}`: {}", file_path.display(), err)
54 })
55 }
56 }
57 .map(|content| {
58 if content.is_empty() {
59 "".to_owned()
60 } else if content.contains('\n') {
61 let mut equal_count = 0;
62
63 let close_comment = loop {
64 let close_comment = format!("]{}]", "=".repeat(equal_count));
65 if !content.contains(&close_comment) {
66 break close_comment;
67 }
68 equal_count += 1;
69 };
70
71 format!(
72 "--[{}[\n{}\n{}",
73 "=".repeat(equal_count),
74 content,
75 close_comment
76 )
77 } else {
78 format!("--{}", content)
79 }
80 })
81 })
82 .clone()
83 }
84}
85
86impl Rule for AppendTextComment {
87 fn process(&self, block: &mut Block, context: &Context) -> RuleProcessResult {
88 let text = self.text(context.project_location())?;
89
90 if text.is_empty() {
91 return Ok(());
92 }
93
94 let shift_lines = text.lines().count();
95 ShiftTokenLine::new(shift_lines as isize).flawless_process(block, context);
96
97 match self.location {
98 AppendLocation::Start => {
99 self.location
100 .append_comment(block.mutate_first_token(), text);
101 }
102 AppendLocation::End => {
103 self.location
104 .append_comment(block.mutate_last_token(), text);
105 }
106 }
107
108 Ok(())
109 }
110}
111
112impl RuleConfiguration for AppendTextComment {
113 fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError> {
114 verify_required_any_properties(&properties, &["text", "file"])?;
115 verify_property_collisions(&properties, &["text", "file"])?;
116
117 for (key, value) in properties {
118 match key.as_str() {
119 "text" => {
120 self.text_content = TextContent::Value(value.expect_string(&key)?);
121 }
122 "file" => {
123 self.text_content =
124 TextContent::FilePath(PathBuf::from(value.expect_string(&key)?));
125 }
126 "location" => {
127 self.location = match value.expect_string(&key)?.as_str() {
128 "start" => AppendLocation::Start,
129 "end" => AppendLocation::End,
130 unexpected => {
131 return Err(RuleConfigurationError::UnexpectedValue {
132 property: "location".to_owned(),
133 message: format!(
134 "invalid value `{}` (must be `start` or `end`)",
135 unexpected
136 ),
137 })
138 }
139 };
140 }
141 _ => return Err(RuleConfigurationError::UnexpectedProperty(key)),
142 }
143 }
144
145 Ok(())
146 }
147
148 fn get_name(&self) -> &'static str {
149 APPEND_TEXT_COMMENT_RULE_NAME
150 }
151
152 fn serialize_to_properties(&self) -> RuleProperties {
153 let mut properties = RuleProperties::new();
154
155 match self.location {
156 AppendLocation::Start => {}
157 AppendLocation::End => {
158 properties.insert("location".to_owned(), "end".into());
159 }
160 }
161
162 match &self.text_content {
163 TextContent::None => {}
164 TextContent::Value(value) => {
165 properties.insert("text".to_owned(), value.into());
166 }
167 TextContent::FilePath(file_path) => {
168 properties.insert(
169 "file".to_owned(),
170 file_path.to_string_lossy().to_string().into(),
171 );
172 }
173 }
174
175 properties
176 }
177}
178
179#[derive(Debug, PartialEq, Eq)]
180enum TextContent {
181 None,
182 Value(String),
183 FilePath(PathBuf),
184}
185
186impl Default for TextContent {
187 fn default() -> Self {
188 Self::None
189 }
190}
191
192#[derive(Debug, PartialEq, Eq)]
193enum AppendLocation {
194 Start,
195 End,
196}
197
198impl AppendLocation {
199 fn append_comment(&self, token: &mut Token, comment: String) {
200 match self {
201 AppendLocation::Start => {
202 token.push_leading_trivia(TriviaKind::Comment.with_content(comment));
203 }
204 AppendLocation::End => {
205 token.push_trailing_trivia(TriviaKind::Comment.with_content(comment));
206 }
207 }
208 }
209}
210
211impl Default for AppendLocation {
212 fn default() -> Self {
213 Self::Start
214 }
215}
216
217#[cfg(test)]
218mod test {
219 use super::*;
220 use crate::rules::Rule;
221
222 use insta::assert_json_snapshot;
223
224 #[test]
225 fn serialize_rule_with_text() {
226 let rule: Box<dyn Rule> = Box::new(AppendTextComment::new("content"));
227
228 assert_json_snapshot!("append_text_comment_with_text", rule);
229 }
230
231 #[test]
232 fn serialize_rule_with_text_at_end() {
233 let rule: Box<dyn Rule> = Box::new(AppendTextComment::new("content").at_end());
234
235 assert_json_snapshot!("append_text_comment_with_text_at_end", rule);
236 }
237
238 #[test]
239 fn configure_with_extra_field_error() {
240 let result = json5::from_str::<Box<dyn Rule>>(
241 r#"{
242 rule: 'append_text_comment',
243 text: '',
244 prop: "something",
245 }"#,
246 );
247 pretty_assertions::assert_eq!(result.unwrap_err().to_string(), "unexpected field 'prop'");
248 }
249
250 #[test]
251 fn configure_with_invalid_location_error() {
252 let result = json5::from_str::<Box<dyn Rule>>(
253 r#"{
254 rule: 'append_text_comment',
255 text: 'hello',
256 location: 'oops',
257 }"#,
258 );
259 pretty_assertions::assert_eq!(result.unwrap_err().to_string(), "unexpected value for field 'location': invalid value `oops` (must be `start` or `end`)");
260 }
261}