nginx_lint_plugin/types.rs
1//! Core types for plugin development.
2//!
3//! This module provides the fundamental types needed to build nginx-lint plugins:
4//!
5//! - [`Plugin`] - The trait every plugin must implement
6//! - [`PluginSpec`] - Plugin metadata (name, category, description, examples)
7//! - [`LintError`] - Lint errors reported by plugins
8//! - [`ErrorBuilder`] - Helper for creating errors with pre-filled rule/category
9//! - [`Fix`] - Autofix actions (replace, delete, insert)
10//! - [`ConfigExt`] - Extension trait for traversing the nginx config AST
11//! - [`DirectiveExt`] - Extension trait for inspecting and modifying directives
12//! - [`DirectiveWithContext`] - A directive paired with its parent block context
13//!
14//! These types mirror the nginx-lint AST and error types for use in WASM plugins.
15
16use serde::{Deserialize, Serialize};
17
18/// Current API version for the plugin SDK
19pub const API_VERSION: &str = "1.0";
20
21/// Plugin metadata describing a lint rule.
22///
23/// Created via [`PluginSpec::new()`] and configured with builder methods.
24///
25/// # Example
26///
27/// ```
28/// use nginx_lint_plugin::PluginSpec;
29///
30/// let spec = PluginSpec::new("my-rule", "security", "Short description")
31/// .with_severity("warning")
32/// .with_why("Detailed explanation of why this rule exists.")
33/// .with_bad_example("server {\n bad_directive on;\n}")
34/// .with_good_example("server {\n bad_directive off;\n}")
35/// .with_references(vec![
36/// "https://nginx.org/en/docs/...".to_string(),
37/// ]);
38///
39/// assert_eq!(spec.name, "my-rule");
40/// assert_eq!(spec.category, "security");
41/// assert_eq!(spec.severity, Some("warning".to_string()));
42/// ```
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct PluginSpec {
45 /// Unique name for the rule (e.g., "my-custom-rule")
46 pub name: String,
47 /// Category (e.g., "security", "style", "best_practices", "custom")
48 pub category: String,
49 /// Human-readable description
50 pub description: String,
51 /// API version the plugin uses for input/output format
52 pub api_version: String,
53 /// Severity level (error, warning)
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub severity: Option<String>,
56 /// Why this rule exists (detailed explanation)
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub why: Option<String>,
59 /// Example of bad configuration
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub bad_example: Option<String>,
62 /// Example of good configuration
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub good_example: Option<String>,
65 /// References (URLs, documentation links)
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub references: Option<Vec<String>>,
68}
69
70impl PluginSpec {
71 /// Create a new PluginSpec with the current API version
72 pub fn new(
73 name: impl Into<String>,
74 category: impl Into<String>,
75 description: impl Into<String>,
76 ) -> Self {
77 Self {
78 name: name.into(),
79 category: category.into(),
80 description: description.into(),
81 api_version: API_VERSION.to_string(),
82 severity: None,
83 why: None,
84 bad_example: None,
85 good_example: None,
86 references: None,
87 }
88 }
89
90 /// Set the severity level
91 pub fn with_severity(mut self, severity: impl Into<String>) -> Self {
92 self.severity = Some(severity.into());
93 self
94 }
95
96 /// Set the why documentation
97 pub fn with_why(mut self, why: impl Into<String>) -> Self {
98 self.why = Some(why.into());
99 self
100 }
101
102 /// Set the bad example
103 pub fn with_bad_example(mut self, example: impl Into<String>) -> Self {
104 self.bad_example = Some(example.into());
105 self
106 }
107
108 /// Set the good example
109 pub fn with_good_example(mut self, example: impl Into<String>) -> Self {
110 self.good_example = Some(example.into());
111 self
112 }
113
114 /// Set references
115 pub fn with_references(mut self, refs: Vec<String>) -> Self {
116 self.references = Some(refs);
117 self
118 }
119
120 /// Create an error builder that uses this plugin's name and category
121 ///
122 /// This reduces boilerplate when creating errors in the check method.
123 ///
124 /// # Example
125 ///
126 /// ```
127 /// use nginx_lint_plugin::{PluginSpec, Severity};
128 ///
129 /// let spec = PluginSpec::new("my-rule", "security", "Check something");
130 /// let err = spec.error_builder();
131 ///
132 /// // Instead of:
133 /// // LintError::warning("my-rule", "security", "message", 10, 5)
134 /// // Use:
135 /// let warning = err.warning("message", 10, 5);
136 /// assert_eq!(warning.rule, "my-rule");
137 /// assert_eq!(warning.category, "security");
138 /// assert_eq!(warning.severity, Severity::Warning);
139 /// ```
140 pub fn error_builder(&self) -> ErrorBuilder {
141 ErrorBuilder {
142 rule: self.name.clone(),
143 category: self.category.clone(),
144 }
145 }
146}
147
148/// Builder for creating LintError with pre-filled rule and category
149#[derive(Debug, Clone)]
150pub struct ErrorBuilder {
151 rule: String,
152 category: String,
153}
154
155impl ErrorBuilder {
156 /// Create an error with Error severity
157 pub fn error(&self, message: &str, line: usize, column: usize) -> LintError {
158 LintError::error(&self.rule, &self.category, message, line, column)
159 }
160
161 /// Create an error with Warning severity
162 pub fn warning(&self, message: &str, line: usize, column: usize) -> LintError {
163 LintError::warning(&self.rule, &self.category, message, line, column)
164 }
165
166 /// Create an error from a directive's location
167 pub fn error_at(&self, message: &str, directive: &(impl DirectiveExt + ?Sized)) -> LintError {
168 self.error(message, directive.line(), directive.column())
169 }
170
171 /// Create a warning from a directive's location
172 pub fn warning_at(&self, message: &str, directive: &(impl DirectiveExt + ?Sized)) -> LintError {
173 self.warning(message, directive.line(), directive.column())
174 }
175}
176
177/// Severity level for lint errors
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(rename_all = "lowercase")]
180pub enum Severity {
181 Error,
182 Warning,
183}
184
185/// Represents a fix that can be applied to automatically resolve a lint error.
186///
187/// Fixes can operate in two modes:
188///
189/// - **Line-based**: Operate on entire lines (delete, insert after, replace text on a line)
190/// - **Range-based**: Operate on byte offsets for precise edits (multiple fixes per line)
191///
192/// Use the convenience methods on [`DirectiveExt`] to create fixes from directives:
193///
194/// ```
195/// use nginx_lint_plugin::prelude::*;
196///
197/// let config = nginx_lint_plugin::parse_string("server_tokens on;").unwrap();
198/// let directive = config.all_directives().next().unwrap();
199///
200/// // Replace the entire directive
201/// let fix = directive.replace_with("server_tokens off;");
202/// assert!(fix.is_range_based());
203///
204/// // Delete the directive's line
205/// let fix = directive.delete_line();
206/// assert!(fix.is_range_based());
207///
208/// // Insert a new line after the directive
209/// let fix = directive.insert_after("add_header X-Frame-Options DENY;");
210/// assert!(fix.is_range_based());
211///
212/// // Insert before the directive
213/// let fix = directive.insert_before("# Security headers");
214/// assert!(fix.is_range_based());
215/// ```
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct Fix {
218 /// Line number where the fix should be applied (1-indexed)
219 pub line: usize,
220 /// The original text to replace (if None and new_text is empty, delete the line)
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub old_text: Option<String>,
223 /// The new text to insert (empty string with old_text=None means delete)
224 pub new_text: String,
225 /// Whether to delete the entire line
226 #[serde(default)]
227 pub delete_line: bool,
228 /// Whether to insert new_text as a new line after the specified line
229 #[serde(default)]
230 pub insert_after: bool,
231 /// Start byte offset for range-based fix (0-indexed, inclusive)
232 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub start_offset: Option<usize>,
234 /// End byte offset for range-based fix (0-indexed, exclusive)
235 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub end_offset: Option<usize>,
237}
238
239impl Fix {
240 /// Create a fix that deletes an entire line
241 #[deprecated(note = "Use Fix::replace_range() for offset-based fixes instead")]
242 pub fn delete(line: usize) -> Self {
243 Self {
244 line,
245 old_text: None,
246 new_text: String::new(),
247 delete_line: true,
248 insert_after: false,
249 start_offset: None,
250 end_offset: None,
251 }
252 }
253
254 /// Create a fix that inserts a new line after the specified line
255 #[deprecated(note = "Use Fix::replace_range() for offset-based fixes instead")]
256 pub fn insert_after(line: usize, new_text: &str) -> Self {
257 Self {
258 line,
259 old_text: None,
260 new_text: new_text.to_string(),
261 delete_line: false,
262 insert_after: true,
263 start_offset: None,
264 end_offset: None,
265 }
266 }
267
268 /// Create a range-based fix that replaces bytes from start to end offset
269 ///
270 /// This allows multiple fixes on the same line as long as their ranges don't overlap.
271 pub fn replace_range(start_offset: usize, end_offset: usize, new_text: &str) -> Self {
272 Self {
273 line: 0, // Not used for range-based fixes
274 old_text: None,
275 new_text: new_text.to_string(),
276 delete_line: false,
277 insert_after: false,
278 start_offset: Some(start_offset),
279 end_offset: Some(end_offset),
280 }
281 }
282
283 /// Check if this is a range-based fix
284 pub fn is_range_based(&self) -> bool {
285 self.start_offset.is_some() && self.end_offset.is_some()
286 }
287}
288
289/// A lint error reported by a plugin.
290///
291/// Create errors using [`LintError::error()`] / [`LintError::warning()`] directly,
292/// or more conveniently via [`ErrorBuilder`] (obtained from [`PluginSpec::error_builder()`]):
293///
294/// ```
295/// use nginx_lint_plugin::prelude::*;
296///
297/// let spec = PluginSpec::new("my-rule", "security", "Check something");
298/// let err = spec.error_builder();
299///
300/// // Warning at a specific line/column
301/// let warning = err.warning("message", 10, 5);
302/// assert_eq!(warning.line, Some(10));
303///
304/// // Warning at a directive's location (most common pattern)
305/// let config = nginx_lint_plugin::parse_string("autoindex on;").unwrap();
306/// let directive = config.all_directives().next().unwrap();
307/// let error = err.warning_at("use 'off'", directive)
308/// .with_fix(directive.replace_with("autoindex off;"));
309/// assert_eq!(error.fixes.len(), 1);
310/// ```
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct LintError {
313 pub rule: String,
314 pub category: String,
315 pub message: String,
316 pub severity: Severity,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub line: Option<usize>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub column: Option<usize>,
321 #[serde(default, skip_serializing_if = "Vec::is_empty")]
322 pub fixes: Vec<Fix>,
323}
324
325impl LintError {
326 /// Create a new error with Error severity
327 pub fn error(rule: &str, category: &str, message: &str, line: usize, column: usize) -> Self {
328 Self {
329 rule: rule.to_string(),
330 category: category.to_string(),
331 message: message.to_string(),
332 severity: Severity::Error,
333 line: if line > 0 { Some(line) } else { None },
334 column: if column > 0 { Some(column) } else { None },
335 fixes: Vec::new(),
336 }
337 }
338
339 /// Create a new error with Warning severity
340 pub fn warning(rule: &str, category: &str, message: &str, line: usize, column: usize) -> Self {
341 Self {
342 rule: rule.to_string(),
343 category: category.to_string(),
344 message: message.to_string(),
345 severity: Severity::Warning,
346 line: if line > 0 { Some(line) } else { None },
347 column: if column > 0 { Some(column) } else { None },
348 fixes: Vec::new(),
349 }
350 }
351
352 /// Attach a fix to this error
353 pub fn with_fix(mut self, fix: Fix) -> Self {
354 self.fixes.push(fix);
355 self
356 }
357
358 /// Attach multiple fixes to this error
359 pub fn with_fixes(mut self, fixes: Vec<Fix>) -> Self {
360 self.fixes.extend(fixes);
361 self
362 }
363}
364
365/// Trait that all plugins must implement.
366///
367/// A plugin consists of two parts:
368/// - **Metadata** ([`spec()`](Plugin::spec)) describing the rule name, category, severity, and documentation
369/// - **Logic** ([`check()`](Plugin::check)) that inspects the parsed nginx config and reports errors
370///
371/// Plugins must also derive [`Default`], which is used by [`export_component_plugin!`](crate::export_component_plugin)
372/// to instantiate the plugin.
373///
374/// # Example
375///
376/// ```
377/// use nginx_lint_plugin::prelude::*;
378///
379/// #[derive(Default)]
380/// pub struct MyPlugin;
381///
382/// impl Plugin for MyPlugin {
383/// fn spec(&self) -> PluginSpec {
384/// PluginSpec::new("my-rule", "security", "Check for something")
385/// .with_severity("warning")
386/// }
387///
388/// fn check(&self, config: &Config, _path: &str) -> Vec<LintError> {
389/// let mut errors = Vec::new();
390/// let err = self.spec().error_builder();
391///
392/// for ctx in config.all_directives_with_context() {
393/// if ctx.is_inside("http") && ctx.directive.is("bad_directive") {
394/// errors.push(err.warning_at("Avoid bad_directive", ctx.directive));
395/// }
396/// }
397/// errors
398/// }
399/// }
400///
401/// // export_component_plugin!(MyPlugin); // Required for WASM build
402///
403/// // Verify it works
404/// let plugin = MyPlugin;
405/// let config = nginx_lint_plugin::parse_string("http { bad_directive on; }").unwrap();
406/// let errors = plugin.check(&config, "test.conf");
407/// assert_eq!(errors.len(), 1);
408/// ```
409pub trait Plugin: Default {
410 /// Return plugin metadata.
411 ///
412 /// This is called once at plugin load time. Use [`PluginSpec::new()`] to create
413 /// the spec, then chain builder methods like [`with_severity()`](PluginSpec::with_severity),
414 /// [`with_why()`](PluginSpec::with_why), [`with_bad_example()`](PluginSpec::with_bad_example), etc.
415 fn spec(&self) -> PluginSpec;
416
417 /// Check the configuration and return any lint errors.
418 ///
419 /// Called once per file being linted. The `config` parameter contains the parsed
420 /// AST of the nginx configuration file. The `path` parameter is the file path
421 /// being checked (useful for error messages).
422 ///
423 /// Use [`config.all_directives()`](ConfigExt::all_directives) for simple iteration
424 /// or [`config.all_directives_with_context()`](ConfigExt::all_directives_with_context)
425 /// when you need to know the parent block context.
426 fn check(&self, config: &Config, path: &str) -> Vec<LintError>;
427}
428
429// Re-export AST types from nginx-lint-common
430pub use nginx_lint_common::parser::ast::{
431 Argument, ArgumentValue, Block, Comment, Config, ConfigItem, Directive, Position, Span,
432};
433pub use nginx_lint_common::parser::context::{AllDirectivesWithContextIter, DirectiveWithContext};
434
435/// Extension trait for [`Config`] providing iteration and include-context helpers.
436///
437/// This trait is automatically available when using `use nginx_lint_plugin::prelude::*`.
438///
439/// # Traversal
440///
441/// Two traversal methods are provided:
442///
443/// - [`all_directives()`](ConfigExt::all_directives) - Simple recursive iteration over all directives
444/// - [`all_directives_with_context()`](ConfigExt::all_directives_with_context) - Iteration with
445/// parent block context (e.g., know if a directive is inside `http`, `server`, `location`)
446///
447/// # Include Context
448///
449/// When nginx-lint processes `include` directives, the included file's [`Config`] receives
450/// an `include_context` field recording the parent block names. For example, a file included
451/// from `http { server { include conf.d/*.conf; } }` would have
452/// `include_context = ["http", "server"]`.
453///
454/// The `is_included_from_*` methods check this context:
455///
456/// ```
457/// use nginx_lint_plugin::prelude::*;
458///
459/// let mut config = nginx_lint_plugin::parse_string("server { listen 80; }").unwrap();
460/// assert!(!config.is_included_from_http());
461///
462/// // Simulate being included from http context
463/// config.include_context = vec!["http".to_string()];
464/// assert!(config.is_included_from_http());
465/// ```
466pub trait ConfigExt {
467 /// Iterate over all directives recursively.
468 ///
469 /// Traverses the entire config tree depth-first, yielding each [`Directive`].
470 fn all_directives(&self) -> nginx_lint_common::parser::ast::AllDirectives<'_>;
471
472 /// Iterate over all directives with parent context information.
473 ///
474 /// Each item is a [`DirectiveWithContext`] that includes the parent block stack.
475 /// This is the recommended traversal method for most plugins, as it allows
476 /// checking whether a directive is inside a specific block (e.g., `http`, `server`).
477 fn all_directives_with_context(&self) -> AllDirectivesWithContextIter<'_>;
478
479 /// Check if this config is included from within a specific context.
480 fn is_included_from(&self, context: &str) -> bool;
481
482 /// Check if this config is included from within `http` context.
483 fn is_included_from_http(&self) -> bool;
484
485 /// Check if this config is included from within `http > server` context.
486 fn is_included_from_http_server(&self) -> bool;
487
488 /// Check if this config is included from within `http > ... > location` context.
489 fn is_included_from_http_location(&self) -> bool;
490
491 /// Check if this config is included from within `stream` context.
492 fn is_included_from_stream(&self) -> bool;
493
494 /// Get the immediate parent context (last element in include_context).
495 fn immediate_parent_context(&self) -> Option<&str>;
496}
497
498impl ConfigExt for Config {
499 fn all_directives(&self) -> nginx_lint_common::parser::ast::AllDirectives<'_> {
500 // Delegate to Config's inherent method
501 Config::all_directives(self)
502 }
503
504 fn all_directives_with_context(&self) -> AllDirectivesWithContextIter<'_> {
505 // Delegate to Config's inherent method
506 Config::all_directives_with_context(self)
507 }
508
509 fn is_included_from(&self, context: &str) -> bool {
510 Config::is_included_from(self, context)
511 }
512
513 fn is_included_from_http(&self) -> bool {
514 Config::is_included_from_http(self)
515 }
516
517 fn is_included_from_http_server(&self) -> bool {
518 Config::is_included_from_http_server(self)
519 }
520
521 fn is_included_from_http_location(&self) -> bool {
522 Config::is_included_from_http_location(self)
523 }
524
525 fn is_included_from_stream(&self) -> bool {
526 Config::is_included_from_stream(self)
527 }
528
529 fn immediate_parent_context(&self) -> Option<&str> {
530 Config::immediate_parent_context(self)
531 }
532}
533
534/// Extension trait for [`Directive`] providing inspection and fix-generation helpers.
535///
536/// This trait adds convenience methods to [`Directive`] for:
537/// - **Inspection**: [`is()`](DirectiveExt::is), [`first_arg()`](DirectiveExt::first_arg),
538/// [`has_arg()`](DirectiveExt::has_arg), etc.
539/// - **Fix generation**: [`replace_with()`](DirectiveExt::replace_with),
540/// [`delete_line()`](DirectiveExt::delete_line), [`insert_after()`](DirectiveExt::insert_after), etc.
541///
542/// # Example
543///
544/// ```
545/// use nginx_lint_plugin::prelude::*;
546///
547/// let config = nginx_lint_plugin::parse_string(
548/// "proxy_pass http://backend;"
549/// ).unwrap();
550/// let directive = config.all_directives().next().unwrap();
551///
552/// assert!(directive.is("proxy_pass"));
553/// assert_eq!(directive.first_arg(), Some("http://backend"));
554/// assert_eq!(directive.arg_count(), 1);
555///
556/// // Generate a fix to replace the directive
557/// let fix = directive.replace_with("proxy_pass http://new-backend;");
558/// assert!(fix.is_range_based());
559/// ```
560pub trait DirectiveExt {
561 /// Check if the directive has the given name.
562 fn is(&self, name: &str) -> bool;
563 /// Get the first argument's string value, if any.
564 fn first_arg(&self) -> Option<&str>;
565 /// Check if the first argument equals the given value.
566 fn first_arg_is(&self, value: &str) -> bool;
567 /// Get the argument at the given index.
568 fn arg_at(&self, index: usize) -> Option<&str>;
569 /// Get the last argument's string value, if any.
570 fn last_arg(&self) -> Option<&str>;
571 /// Check if any argument equals the given value.
572 fn has_arg(&self, value: &str) -> bool;
573 /// Return the number of arguments.
574 fn arg_count(&self) -> usize;
575 /// Get the start line number (1-based).
576 fn line(&self) -> usize;
577 /// Get the start column number (1-based).
578 fn column(&self) -> usize;
579 /// Get the byte offset including leading whitespace.
580 fn full_start_offset(&self) -> usize;
581 /// Create a [`Fix`] that replaces this directive with new text, preserving indentation.
582 fn replace_with(&self, new_text: &str) -> Fix;
583 /// Create a [`Fix`] that deletes this directive's line.
584 fn delete_line(&self) -> Fix;
585 /// Create a [`Fix`] that inserts a new line after this directive, matching indentation.
586 fn insert_after(&self, new_text: &str) -> Fix;
587 /// Create a [`Fix`] that inserts multiple new lines after this directive.
588 fn insert_after_many(&self, lines: &[&str]) -> Fix;
589 /// Create a [`Fix`] that inserts a new line before this directive, matching indentation.
590 fn insert_before(&self, new_text: &str) -> Fix;
591 /// Create a [`Fix`] that inserts multiple new lines before this directive.
592 fn insert_before_many(&self, lines: &[&str]) -> Fix;
593}
594
595impl DirectiveExt for Directive {
596 fn is(&self, name: &str) -> bool {
597 self.name == name
598 }
599
600 fn first_arg(&self) -> Option<&str> {
601 self.args.first().map(|a| a.as_str())
602 }
603
604 fn first_arg_is(&self, value: &str) -> bool {
605 self.first_arg() == Some(value)
606 }
607
608 fn arg_at(&self, index: usize) -> Option<&str> {
609 self.args.get(index).map(|a| a.as_str())
610 }
611
612 fn last_arg(&self) -> Option<&str> {
613 self.args.last().map(|a| a.as_str())
614 }
615
616 fn has_arg(&self, value: &str) -> bool {
617 self.args.iter().any(|a| a.as_str() == value)
618 }
619
620 fn arg_count(&self) -> usize {
621 self.args.len()
622 }
623
624 fn line(&self) -> usize {
625 self.span.start.line
626 }
627
628 fn column(&self) -> usize {
629 self.span.start.column
630 }
631
632 fn full_start_offset(&self) -> usize {
633 self.span.start.offset - self.leading_whitespace.len()
634 }
635
636 fn replace_with(&self, new_text: &str) -> Fix {
637 let start = self.full_start_offset();
638 let end = self.span.end.offset;
639 let fixed = format!("{}{}", self.leading_whitespace, new_text);
640 Fix::replace_range(start, end, &fixed)
641 }
642
643 fn delete_line(&self) -> Fix {
644 let start = self.full_start_offset();
645 let end = self.span.end.offset + self.trailing_whitespace.len();
646 // Include trailing comment if present
647 let end = if let Some(ref comment) = self.trailing_comment {
648 comment.span.end.offset + comment.trailing_whitespace.len()
649 } else {
650 end
651 };
652 // Remove the preceding newline to actually delete the line
653 // (if not the first line)
654 if start > 0 {
655 Fix::replace_range(start - 1, end, "")
656 } else {
657 // First line: remove trailing newline instead
658 Fix::replace_range(start, end + 1, "")
659 }
660 }
661
662 fn insert_after(&self, new_text: &str) -> Fix {
663 self.insert_after_many(&[new_text])
664 }
665
666 fn insert_after_many(&self, lines: &[&str]) -> Fix {
667 let indent = " ".repeat(self.span.start.column.saturating_sub(1));
668 let fix_text: String = lines
669 .iter()
670 .map(|line| format!("\n{}{}", indent, line))
671 .collect();
672 let insert_offset = self.span.end.offset;
673 Fix::replace_range(insert_offset, insert_offset, &fix_text)
674 }
675
676 fn insert_before(&self, new_text: &str) -> Fix {
677 self.insert_before_many(&[new_text])
678 }
679
680 fn insert_before_many(&self, lines: &[&str]) -> Fix {
681 let indent = " ".repeat(self.span.start.column.saturating_sub(1));
682 let fix_text: String = lines
683 .iter()
684 .map(|line| format!("{}{}\n", indent, line))
685 .collect();
686 let line_start_offset = self.span.start.offset - (self.span.start.column - 1);
687 Fix::replace_range(line_start_offset, line_start_offset, &fix_text)
688 }
689}
690
691impl<T: DirectiveExt + ?Sized> DirectiveExt for &T {
692 fn is(&self, name: &str) -> bool {
693 (**self).is(name)
694 }
695 fn first_arg(&self) -> Option<&str> {
696 (**self).first_arg()
697 }
698 fn first_arg_is(&self, value: &str) -> bool {
699 (**self).first_arg_is(value)
700 }
701 fn arg_at(&self, index: usize) -> Option<&str> {
702 (**self).arg_at(index)
703 }
704 fn last_arg(&self) -> Option<&str> {
705 (**self).last_arg()
706 }
707 fn has_arg(&self, value: &str) -> bool {
708 (**self).has_arg(value)
709 }
710 fn arg_count(&self) -> usize {
711 (**self).arg_count()
712 }
713 fn line(&self) -> usize {
714 (**self).line()
715 }
716 fn column(&self) -> usize {
717 (**self).column()
718 }
719 fn full_start_offset(&self) -> usize {
720 (**self).full_start_offset()
721 }
722 fn replace_with(&self, new_text: &str) -> Fix {
723 (**self).replace_with(new_text)
724 }
725 fn delete_line(&self) -> Fix {
726 (**self).delete_line()
727 }
728 fn insert_after(&self, new_text: &str) -> Fix {
729 (**self).insert_after(new_text)
730 }
731 fn insert_after_many(&self, lines: &[&str]) -> Fix {
732 (**self).insert_after_many(lines)
733 }
734 fn insert_before(&self, new_text: &str) -> Fix {
735 (**self).insert_before(new_text)
736 }
737 fn insert_before_many(&self, lines: &[&str]) -> Fix {
738 (**self).insert_before_many(lines)
739 }
740}
741
742impl DirectiveExt for Box<Directive> {
743 fn is(&self, name: &str) -> bool {
744 (**self).is(name)
745 }
746 fn first_arg(&self) -> Option<&str> {
747 (**self).first_arg()
748 }
749 fn first_arg_is(&self, value: &str) -> bool {
750 (**self).first_arg_is(value)
751 }
752 fn arg_at(&self, index: usize) -> Option<&str> {
753 (**self).arg_at(index)
754 }
755 fn last_arg(&self) -> Option<&str> {
756 (**self).last_arg()
757 }
758 fn has_arg(&self, value: &str) -> bool {
759 (**self).has_arg(value)
760 }
761 fn arg_count(&self) -> usize {
762 (**self).arg_count()
763 }
764 fn line(&self) -> usize {
765 (**self).line()
766 }
767 fn column(&self) -> usize {
768 (**self).column()
769 }
770 fn full_start_offset(&self) -> usize {
771 (**self).full_start_offset()
772 }
773 fn replace_with(&self, new_text: &str) -> Fix {
774 (**self).replace_with(new_text)
775 }
776 fn delete_line(&self) -> Fix {
777 (**self).delete_line()
778 }
779 fn insert_after(&self, new_text: &str) -> Fix {
780 (**self).insert_after(new_text)
781 }
782 fn insert_after_many(&self, lines: &[&str]) -> Fix {
783 (**self).insert_after_many(lines)
784 }
785 fn insert_before(&self, new_text: &str) -> Fix {
786 (**self).insert_before(new_text)
787 }
788 fn insert_before_many(&self, lines: &[&str]) -> Fix {
789 (**self).insert_before_many(lines)
790 }
791}
792
793/// Extension trait for Argument to add source reconstruction
794pub trait ArgumentExt {
795 /// Reconstruct the source text for this argument
796 fn to_source(&self) -> String;
797}
798
799impl ArgumentExt for Argument {
800 fn to_source(&self) -> String {
801 match &self.value {
802 ArgumentValue::Literal(s) => s.clone(),
803 ArgumentValue::QuotedString(s) => format!("\"{}\"", s),
804 ArgumentValue::SingleQuotedString(s) => format!("'{}'", s),
805 ArgumentValue::Variable(s) => format!("${}", s),
806 }
807 }
808}