Skip to main content

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.delete_line);
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    pub fn delete(line: usize) -> Self {
242        Self {
243            line,
244            old_text: None,
245            new_text: String::new(),
246            delete_line: true,
247            insert_after: false,
248            start_offset: None,
249            end_offset: None,
250        }
251    }
252
253    /// Create a fix that inserts a new line after the specified line
254    pub fn insert_after(line: usize, new_text: &str) -> Self {
255        Self {
256            line,
257            old_text: None,
258            new_text: new_text.to_string(),
259            delete_line: false,
260            insert_after: true,
261            start_offset: None,
262            end_offset: None,
263        }
264    }
265
266    /// Create a range-based fix that replaces bytes from start to end offset
267    ///
268    /// This allows multiple fixes on the same line as long as their ranges don't overlap.
269    pub fn replace_range(start_offset: usize, end_offset: usize, new_text: &str) -> Self {
270        Self {
271            line: 0, // Not used for range-based fixes
272            old_text: None,
273            new_text: new_text.to_string(),
274            delete_line: false,
275            insert_after: false,
276            start_offset: Some(start_offset),
277            end_offset: Some(end_offset),
278        }
279    }
280
281    /// Check if this is a range-based fix
282    pub fn is_range_based(&self) -> bool {
283        self.start_offset.is_some() && self.end_offset.is_some()
284    }
285}
286
287/// A lint error reported by a plugin.
288///
289/// Create errors using [`LintError::error()`] / [`LintError::warning()`] directly,
290/// or more conveniently via [`ErrorBuilder`] (obtained from [`PluginSpec::error_builder()`]):
291///
292/// ```
293/// use nginx_lint_plugin::prelude::*;
294///
295/// let spec = PluginSpec::new("my-rule", "security", "Check something");
296/// let err = spec.error_builder();
297///
298/// // Warning at a specific line/column
299/// let warning = err.warning("message", 10, 5);
300/// assert_eq!(warning.line, Some(10));
301///
302/// // Warning at a directive's location (most common pattern)
303/// let config = nginx_lint_plugin::parse_string("autoindex on;").unwrap();
304/// let directive = config.all_directives().next().unwrap();
305/// let error = err.warning_at("use 'off'", directive)
306///     .with_fix(directive.replace_with("autoindex off;"));
307/// assert_eq!(error.fixes.len(), 1);
308/// ```
309#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct LintError {
311    pub rule: String,
312    pub category: String,
313    pub message: String,
314    pub severity: Severity,
315    #[serde(default, skip_serializing_if = "Option::is_none")]
316    pub line: Option<usize>,
317    #[serde(default, skip_serializing_if = "Option::is_none")]
318    pub column: Option<usize>,
319    #[serde(default, skip_serializing_if = "Vec::is_empty")]
320    pub fixes: Vec<Fix>,
321}
322
323impl LintError {
324    /// Create a new error with Error severity
325    pub fn error(rule: &str, category: &str, message: &str, line: usize, column: usize) -> Self {
326        Self {
327            rule: rule.to_string(),
328            category: category.to_string(),
329            message: message.to_string(),
330            severity: Severity::Error,
331            line: if line > 0 { Some(line) } else { None },
332            column: if column > 0 { Some(column) } else { None },
333            fixes: Vec::new(),
334        }
335    }
336
337    /// Create a new error with Warning severity
338    pub fn warning(rule: &str, category: &str, message: &str, line: usize, column: usize) -> Self {
339        Self {
340            rule: rule.to_string(),
341            category: category.to_string(),
342            message: message.to_string(),
343            severity: Severity::Warning,
344            line: if line > 0 { Some(line) } else { None },
345            column: if column > 0 { Some(column) } else { None },
346            fixes: Vec::new(),
347        }
348    }
349
350    /// Attach a fix to this error
351    pub fn with_fix(mut self, fix: Fix) -> Self {
352        self.fixes.push(fix);
353        self
354    }
355
356    /// Attach multiple fixes to this error
357    pub fn with_fixes(mut self, fixes: Vec<Fix>) -> Self {
358        self.fixes.extend(fixes);
359        self
360    }
361}
362
363/// Trait that all plugins must implement.
364///
365/// A plugin consists of two parts:
366/// - **Metadata** ([`spec()`](Plugin::spec)) describing the rule name, category, severity, and documentation
367/// - **Logic** ([`check()`](Plugin::check)) that inspects the parsed nginx config and reports errors
368///
369/// Plugins must also derive [`Default`], which is used by [`export_plugin!`](crate::export_plugin)
370/// to instantiate the plugin.
371///
372/// # Example
373///
374/// ```
375/// use nginx_lint_plugin::prelude::*;
376///
377/// #[derive(Default)]
378/// pub struct MyPlugin;
379///
380/// impl Plugin for MyPlugin {
381///     fn spec(&self) -> PluginSpec {
382///         PluginSpec::new("my-rule", "security", "Check for something")
383///             .with_severity("warning")
384///     }
385///
386///     fn check(&self, config: &Config, _path: &str) -> Vec<LintError> {
387///         let mut errors = Vec::new();
388///         let err = self.spec().error_builder();
389///
390///         for ctx in config.all_directives_with_context() {
391///             if ctx.is_inside("http") && ctx.directive.is("bad_directive") {
392///                 errors.push(err.warning_at("Avoid bad_directive", ctx.directive));
393///             }
394///         }
395///         errors
396///     }
397/// }
398///
399/// // export_plugin!(MyPlugin);  // Required for WASM build
400///
401/// // Verify it works
402/// let plugin = MyPlugin;
403/// let config = nginx_lint_plugin::parse_string("http { bad_directive on; }").unwrap();
404/// let errors = plugin.check(&config, "test.conf");
405/// assert_eq!(errors.len(), 1);
406/// ```
407pub trait Plugin: Default {
408    /// Return plugin metadata.
409    ///
410    /// This is called once at plugin load time. Use [`PluginSpec::new()`] to create
411    /// the spec, then chain builder methods like [`with_severity()`](PluginSpec::with_severity),
412    /// [`with_why()`](PluginSpec::with_why), [`with_bad_example()`](PluginSpec::with_bad_example), etc.
413    fn spec(&self) -> PluginSpec;
414
415    /// Check the configuration and return any lint errors.
416    ///
417    /// Called once per file being linted. The `config` parameter contains the parsed
418    /// AST of the nginx configuration file. The `path` parameter is the file path
419    /// being checked (useful for error messages).
420    ///
421    /// Use [`config.all_directives()`](ConfigExt::all_directives) for simple iteration
422    /// or [`config.all_directives_with_context()`](ConfigExt::all_directives_with_context)
423    /// when you need to know the parent block context.
424    fn check(&self, config: &Config, path: &str) -> Vec<LintError>;
425}
426
427// Re-export AST types from nginx-lint-common
428pub use nginx_lint_common::parser::ast::{
429    Argument, ArgumentValue, Block, Comment, Config, ConfigItem, Directive, Position, Span,
430};
431pub use nginx_lint_common::parser::context::{AllDirectivesWithContextIter, DirectiveWithContext};
432
433/// Extension trait for [`Config`] providing iteration and include-context helpers.
434///
435/// This trait is automatically available when using `use nginx_lint_plugin::prelude::*`.
436///
437/// # Traversal
438///
439/// Two traversal methods are provided:
440///
441/// - [`all_directives()`](ConfigExt::all_directives) - Simple recursive iteration over all directives
442/// - [`all_directives_with_context()`](ConfigExt::all_directives_with_context) - Iteration with
443///   parent block context (e.g., know if a directive is inside `http`, `server`, `location`)
444///
445/// # Include Context
446///
447/// When nginx-lint processes `include` directives, the included file's [`Config`] receives
448/// an `include_context` field recording the parent block names. For example, a file included
449/// from `http { server { include conf.d/*.conf; } }` would have
450/// `include_context = ["http", "server"]`.
451///
452/// The `is_included_from_*` methods check this context:
453///
454/// ```
455/// use nginx_lint_plugin::prelude::*;
456///
457/// let mut config = nginx_lint_plugin::parse_string("server { listen 80; }").unwrap();
458/// assert!(!config.is_included_from_http());
459///
460/// // Simulate being included from http context
461/// config.include_context = vec!["http".to_string()];
462/// assert!(config.is_included_from_http());
463/// ```
464pub trait ConfigExt {
465    /// Iterate over all directives recursively.
466    ///
467    /// Traverses the entire config tree depth-first, yielding each [`Directive`].
468    fn all_directives(&self) -> nginx_lint_common::parser::ast::AllDirectives<'_>;
469
470    /// Iterate over all directives with parent context information.
471    ///
472    /// Each item is a [`DirectiveWithContext`] that includes the parent block stack.
473    /// This is the recommended traversal method for most plugins, as it allows
474    /// checking whether a directive is inside a specific block (e.g., `http`, `server`).
475    fn all_directives_with_context(&self) -> AllDirectivesWithContextIter<'_>;
476
477    /// Check if this config is included from within a specific context.
478    fn is_included_from(&self, context: &str) -> bool;
479
480    /// Check if this config is included from within `http` context.
481    fn is_included_from_http(&self) -> bool;
482
483    /// Check if this config is included from within `http > server` context.
484    fn is_included_from_http_server(&self) -> bool;
485
486    /// Check if this config is included from within `http > ... > location` context.
487    fn is_included_from_http_location(&self) -> bool;
488
489    /// Check if this config is included from within `stream` context.
490    fn is_included_from_stream(&self) -> bool;
491
492    /// Get the immediate parent context (last element in include_context).
493    fn immediate_parent_context(&self) -> Option<&str>;
494}
495
496impl ConfigExt for Config {
497    fn all_directives(&self) -> nginx_lint_common::parser::ast::AllDirectives<'_> {
498        // Delegate to Config's inherent method
499        Config::all_directives(self)
500    }
501
502    fn all_directives_with_context(&self) -> AllDirectivesWithContextIter<'_> {
503        // Delegate to Config's inherent method
504        Config::all_directives_with_context(self)
505    }
506
507    fn is_included_from(&self, context: &str) -> bool {
508        Config::is_included_from(self, context)
509    }
510
511    fn is_included_from_http(&self) -> bool {
512        Config::is_included_from_http(self)
513    }
514
515    fn is_included_from_http_server(&self) -> bool {
516        Config::is_included_from_http_server(self)
517    }
518
519    fn is_included_from_http_location(&self) -> bool {
520        Config::is_included_from_http_location(self)
521    }
522
523    fn is_included_from_stream(&self) -> bool {
524        Config::is_included_from_stream(self)
525    }
526
527    fn immediate_parent_context(&self) -> Option<&str> {
528        Config::immediate_parent_context(self)
529    }
530}
531
532/// Extension trait for [`Directive`] providing inspection and fix-generation helpers.
533///
534/// This trait adds convenience methods to [`Directive`] for:
535/// - **Inspection**: [`is()`](DirectiveExt::is), [`first_arg()`](DirectiveExt::first_arg),
536///   [`has_arg()`](DirectiveExt::has_arg), etc.
537/// - **Fix generation**: [`replace_with()`](DirectiveExt::replace_with),
538///   [`delete_line()`](DirectiveExt::delete_line), [`insert_after()`](DirectiveExt::insert_after), etc.
539///
540/// # Example
541///
542/// ```
543/// use nginx_lint_plugin::prelude::*;
544///
545/// let config = nginx_lint_plugin::parse_string(
546///     "proxy_pass http://backend;"
547/// ).unwrap();
548/// let directive = config.all_directives().next().unwrap();
549///
550/// assert!(directive.is("proxy_pass"));
551/// assert_eq!(directive.first_arg(), Some("http://backend"));
552/// assert_eq!(directive.arg_count(), 1);
553///
554/// // Generate a fix to replace the directive
555/// let fix = directive.replace_with("proxy_pass http://new-backend;");
556/// assert!(fix.is_range_based());
557/// ```
558pub trait DirectiveExt {
559    /// Check if the directive has the given name.
560    fn is(&self, name: &str) -> bool;
561    /// Get the first argument's string value, if any.
562    fn first_arg(&self) -> Option<&str>;
563    /// Check if the first argument equals the given value.
564    fn first_arg_is(&self, value: &str) -> bool;
565    /// Get the argument at the given index.
566    fn arg_at(&self, index: usize) -> Option<&str>;
567    /// Get the last argument's string value, if any.
568    fn last_arg(&self) -> Option<&str>;
569    /// Check if any argument equals the given value.
570    fn has_arg(&self, value: &str) -> bool;
571    /// Return the number of arguments.
572    fn arg_count(&self) -> usize;
573    /// Get the start line number (1-based).
574    fn line(&self) -> usize;
575    /// Get the start column number (1-based).
576    fn column(&self) -> usize;
577    /// Get the byte offset including leading whitespace.
578    fn full_start_offset(&self) -> usize;
579    /// Create a [`Fix`] that replaces this directive with new text, preserving indentation.
580    fn replace_with(&self, new_text: &str) -> Fix;
581    /// Create a [`Fix`] that deletes this directive's line.
582    fn delete_line(&self) -> Fix;
583    /// Create a [`Fix`] that inserts a new line after this directive, matching indentation.
584    fn insert_after(&self, new_text: &str) -> Fix;
585    /// Create a [`Fix`] that inserts multiple new lines after this directive.
586    fn insert_after_many(&self, lines: &[&str]) -> Fix;
587    /// Create a [`Fix`] that inserts a new line before this directive, matching indentation.
588    fn insert_before(&self, new_text: &str) -> Fix;
589    /// Create a [`Fix`] that inserts multiple new lines before this directive.
590    fn insert_before_many(&self, lines: &[&str]) -> Fix;
591}
592
593impl DirectiveExt for Directive {
594    fn is(&self, name: &str) -> bool {
595        self.name == name
596    }
597
598    fn first_arg(&self) -> Option<&str> {
599        self.args.first().map(|a| a.as_str())
600    }
601
602    fn first_arg_is(&self, value: &str) -> bool {
603        self.first_arg() == Some(value)
604    }
605
606    fn arg_at(&self, index: usize) -> Option<&str> {
607        self.args.get(index).map(|a| a.as_str())
608    }
609
610    fn last_arg(&self) -> Option<&str> {
611        self.args.last().map(|a| a.as_str())
612    }
613
614    fn has_arg(&self, value: &str) -> bool {
615        self.args.iter().any(|a| a.as_str() == value)
616    }
617
618    fn arg_count(&self) -> usize {
619        self.args.len()
620    }
621
622    fn line(&self) -> usize {
623        self.span.start.line
624    }
625
626    fn column(&self) -> usize {
627        self.span.start.column
628    }
629
630    fn full_start_offset(&self) -> usize {
631        self.span.start.offset - self.leading_whitespace.len()
632    }
633
634    fn replace_with(&self, new_text: &str) -> Fix {
635        let start = self.full_start_offset();
636        let end = self.span.end.offset;
637        let fixed = format!("{}{}", self.leading_whitespace, new_text);
638        Fix::replace_range(start, end, &fixed)
639    }
640
641    fn delete_line(&self) -> Fix {
642        Fix::delete(self.span.start.line)
643    }
644
645    fn insert_after(&self, new_text: &str) -> Fix {
646        self.insert_after_many(&[new_text])
647    }
648
649    fn insert_after_many(&self, lines: &[&str]) -> Fix {
650        let indent = " ".repeat(self.span.start.column.saturating_sub(1));
651        let fix_text: String = lines
652            .iter()
653            .map(|line| format!("\n{}{}", indent, line))
654            .collect();
655        let insert_offset = self.span.end.offset;
656        Fix::replace_range(insert_offset, insert_offset, &fix_text)
657    }
658
659    fn insert_before(&self, new_text: &str) -> Fix {
660        self.insert_before_many(&[new_text])
661    }
662
663    fn insert_before_many(&self, lines: &[&str]) -> Fix {
664        let indent = " ".repeat(self.span.start.column.saturating_sub(1));
665        let fix_text: String = lines
666            .iter()
667            .map(|line| format!("{}{}\n", indent, line))
668            .collect();
669        let line_start_offset = self.span.start.offset - (self.span.start.column - 1);
670        Fix::replace_range(line_start_offset, line_start_offset, &fix_text)
671    }
672}
673
674impl<T: DirectiveExt + ?Sized> DirectiveExt for &T {
675    fn is(&self, name: &str) -> bool {
676        (**self).is(name)
677    }
678    fn first_arg(&self) -> Option<&str> {
679        (**self).first_arg()
680    }
681    fn first_arg_is(&self, value: &str) -> bool {
682        (**self).first_arg_is(value)
683    }
684    fn arg_at(&self, index: usize) -> Option<&str> {
685        (**self).arg_at(index)
686    }
687    fn last_arg(&self) -> Option<&str> {
688        (**self).last_arg()
689    }
690    fn has_arg(&self, value: &str) -> bool {
691        (**self).has_arg(value)
692    }
693    fn arg_count(&self) -> usize {
694        (**self).arg_count()
695    }
696    fn line(&self) -> usize {
697        (**self).line()
698    }
699    fn column(&self) -> usize {
700        (**self).column()
701    }
702    fn full_start_offset(&self) -> usize {
703        (**self).full_start_offset()
704    }
705    fn replace_with(&self, new_text: &str) -> Fix {
706        (**self).replace_with(new_text)
707    }
708    fn delete_line(&self) -> Fix {
709        (**self).delete_line()
710    }
711    fn insert_after(&self, new_text: &str) -> Fix {
712        (**self).insert_after(new_text)
713    }
714    fn insert_after_many(&self, lines: &[&str]) -> Fix {
715        (**self).insert_after_many(lines)
716    }
717    fn insert_before(&self, new_text: &str) -> Fix {
718        (**self).insert_before(new_text)
719    }
720    fn insert_before_many(&self, lines: &[&str]) -> Fix {
721        (**self).insert_before_many(lines)
722    }
723}
724
725impl DirectiveExt for Box<Directive> {
726    fn is(&self, name: &str) -> bool {
727        (**self).is(name)
728    }
729    fn first_arg(&self) -> Option<&str> {
730        (**self).first_arg()
731    }
732    fn first_arg_is(&self, value: &str) -> bool {
733        (**self).first_arg_is(value)
734    }
735    fn arg_at(&self, index: usize) -> Option<&str> {
736        (**self).arg_at(index)
737    }
738    fn last_arg(&self) -> Option<&str> {
739        (**self).last_arg()
740    }
741    fn has_arg(&self, value: &str) -> bool {
742        (**self).has_arg(value)
743    }
744    fn arg_count(&self) -> usize {
745        (**self).arg_count()
746    }
747    fn line(&self) -> usize {
748        (**self).line()
749    }
750    fn column(&self) -> usize {
751        (**self).column()
752    }
753    fn full_start_offset(&self) -> usize {
754        (**self).full_start_offset()
755    }
756    fn replace_with(&self, new_text: &str) -> Fix {
757        (**self).replace_with(new_text)
758    }
759    fn delete_line(&self) -> Fix {
760        (**self).delete_line()
761    }
762    fn insert_after(&self, new_text: &str) -> Fix {
763        (**self).insert_after(new_text)
764    }
765    fn insert_after_many(&self, lines: &[&str]) -> Fix {
766        (**self).insert_after_many(lines)
767    }
768    fn insert_before(&self, new_text: &str) -> Fix {
769        (**self).insert_before(new_text)
770    }
771    fn insert_before_many(&self, lines: &[&str]) -> Fix {
772        (**self).insert_before_many(lines)
773    }
774}
775
776/// Extension trait for Argument to add source reconstruction
777pub trait ArgumentExt {
778    /// Reconstruct the source text for this argument
779    fn to_source(&self) -> String;
780}
781
782impl ArgumentExt for Argument {
783    fn to_source(&self) -> String {
784        match &self.value {
785            ArgumentValue::Literal(s) => s.clone(),
786            ArgumentValue::QuotedString(s) => format!("\"{}\"", s),
787            ArgumentValue::SingleQuotedString(s) => format!("'{}'", s),
788            ArgumentValue::Variable(s) => format!("${}", s),
789        }
790    }
791}