nginx-lint-plugin 0.12.2

Plugin SDK for nginx-lint
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
//! WASM-mode config types backed by host resource handles.
//!
//! These types provide the same API surface as the parser AST types
//! but delegate to host functions via WIT resource handles instead of
//! holding the data directly.
//!
//! **Note**: The `export_component_plugin!` macro uses `reconstruct_config`
//! from `wit_guest` instead of these types, which rebuilds native parser AST
//! types without the `&str` lifetime limitation. These types are provided for
//! advanced manual interop with the WIT config API only.
//!
//! This module does **not** implement `DirectiveExt` because WIT host calls
//! return owned strings, making it impossible to implement `first_arg()`,
//! `arg_at()`, and `last_arg()` which require `Option<&str>`. Use the owned
//! variants (`first_arg_owned()`, `arg_at_owned()`, `last_arg_owned()`) or
//! the standard `reconstruct_config` path instead.

use crate::types::Fix;
use crate::wit_guest::nginx_lint::plugin::config_api;
use crate::wit_guest::nginx_lint::plugin::types as wit_types;

/// Parsed nginx configuration (backed by host resource handle).
pub struct Config {
    handle: config_api::Config,
}

impl Config {
    /// Create from a WIT resource handle.
    ///
    /// Intended for advanced/manual interop with the WIT-generated
    /// config API. The `export_component_plugin!` macro reconstructs
    /// configs via `reconstruct_config` in `wit_guest` instead of
    /// calling this method directly.
    pub fn from_handle(handle: config_api::Config) -> Self {
        Self { handle }
    }

    /// Iterate over all directives recursively with parent context.
    pub fn all_directives_with_context(&self) -> Vec<DirectiveWithContext> {
        self.handle
            .all_directives_with_context()
            .into_iter()
            .map(|ctx| DirectiveWithContext {
                directive: Directive::from_handle(ctx.directive),
                parent_stack: ctx.parent_stack,
                depth: ctx.depth as usize,
            })
            .collect()
    }

    /// Iterate over all directives recursively.
    pub fn all_directives(&self) -> Vec<Directive> {
        self.handle
            .all_directives()
            .into_iter()
            .map(Directive::from_handle)
            .collect()
    }

    /// Get the top-level config items.
    pub fn items(&self) -> Vec<ConfigItem> {
        self.handle
            .items()
            .into_iter()
            .map(ConfigItem::from_wit)
            .collect()
    }

    /// Get the include context (parent block names from include directive).
    pub fn include_context(&self) -> Vec<String> {
        self.handle.include_context()
    }

    /// Check if this config is included from within a specific context.
    pub fn is_included_from(&self, context: &str) -> bool {
        self.handle.is_included_from(context)
    }

    /// Check if included from http context.
    pub fn is_included_from_http(&self) -> bool {
        self.handle.is_included_from_http()
    }

    /// Check if included from http > server context.
    pub fn is_included_from_http_server(&self) -> bool {
        self.handle.is_included_from_http_server()
    }

    /// Check if included from http > ... > location context.
    pub fn is_included_from_http_location(&self) -> bool {
        self.handle.is_included_from_http_location()
    }

    /// Check if included from stream context.
    pub fn is_included_from_stream(&self) -> bool {
        self.handle.is_included_from_stream()
    }

    /// Get the immediate parent context.
    pub fn immediate_parent_context(&self) -> Option<String> {
        self.handle.immediate_parent_context()
    }
}

/// An nginx directive (backed by host resource handle).
pub struct Directive {
    handle: config_api::Directive,
}

impl Directive {
    /// Create from a WIT resource handle.
    pub(crate) fn from_handle(handle: config_api::Directive) -> Self {
        Self { handle }
    }

    /// Get the directive name.
    pub fn name(&self) -> String {
        self.handle.name()
    }

    /// Get all arguments as records.
    pub fn args(&self) -> Vec<ArgumentInfo> {
        self.handle
            .args()
            .into_iter()
            .map(ArgumentInfo::from_wit)
            .collect()
    }

    /// Check if the directive has a block.
    pub fn has_block(&self) -> bool {
        self.handle.has_block()
    }

    /// Get the items inside the directive's block.
    pub fn block_items(&self) -> Vec<ConfigItem> {
        self.handle
            .block_items()
            .into_iter()
            .map(ConfigItem::from_wit)
            .collect()
    }

    /// Check if the block contains raw content (e.g., lua blocks).
    pub fn block_is_raw(&self) -> bool {
        self.handle.block_is_raw()
    }

    /// Get the start byte offset (0-based).
    pub fn start_offset(&self) -> usize {
        self.handle.start_offset() as usize
    }

    /// Get the end byte offset (0-based).
    pub fn end_offset(&self) -> usize {
        self.handle.end_offset() as usize
    }

    /// Get the leading whitespace before the directive.
    pub fn leading_whitespace(&self) -> String {
        self.handle.leading_whitespace()
    }

    /// Get the trailing whitespace after the directive.
    pub fn trailing_whitespace(&self) -> String {
        self.handle.trailing_whitespace()
    }

    /// Get the space before the terminator (; or {).
    pub fn space_before_terminator(&self) -> String {
        self.handle.space_before_terminator()
    }
}

/// Query and fix methods for WASM-mode Directive.
///
/// These methods return owned strings since WIT host calls return by value.
/// For code that needs `&str` references, use `reconstruct_config` which
/// builds native parser AST types.
impl Directive {
    /// Check if the directive has the given name.
    pub fn is(&self, name: &str) -> bool {
        self.handle.is(name)
    }

    /// Get the first argument value.
    pub fn first_arg_owned(&self) -> Option<String> {
        self.handle.first_arg()
    }

    /// Check if the first argument equals the given value.
    pub fn first_arg_is(&self, value: &str) -> bool {
        self.handle.first_arg_is(value)
    }

    /// Get the argument at the given index.
    pub fn arg_at_owned(&self, index: usize) -> Option<String> {
        self.handle.arg_at(index as u32)
    }

    /// Get the last argument value.
    pub fn last_arg_owned(&self) -> Option<String> {
        self.handle.last_arg()
    }

    /// Check if any argument equals the given value.
    pub fn has_arg(&self, value: &str) -> bool {
        self.handle.has_arg(value)
    }

    /// Return the number of arguments.
    pub fn arg_count(&self) -> usize {
        self.handle.arg_count() as usize
    }

    /// Get the start line number (1-based).
    pub fn line(&self) -> usize {
        self.handle.line() as usize
    }

    /// Get the start column number (1-based).
    pub fn column(&self) -> usize {
        self.handle.column() as usize
    }

    /// Get the full start offset including leading whitespace.
    pub fn full_start_offset(&self) -> usize {
        self.start_offset() - self.leading_whitespace().len()
    }

    /// Create a fix that replaces this directive with new text.
    pub fn replace_with(&self, new_text: &str) -> Fix {
        convert_wit_fix(self.handle.replace_with(new_text))
    }

    /// Create a fix that deletes this directive's line.
    pub fn delete_line(&self) -> Fix {
        convert_wit_fix(self.handle.delete_line_fix())
    }

    /// Create a fix that inserts a new line after this directive.
    pub fn insert_after(&self, new_text: &str) -> Fix {
        convert_wit_fix(self.handle.insert_after(new_text))
    }

    /// Create a fix that inserts multiple lines after this directive.
    pub fn insert_after_many(&self, lines: &[&str]) -> Fix {
        let owned: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
        convert_wit_fix(self.handle.insert_after_many(&owned))
    }

    /// Create a fix that inserts a new line before this directive.
    pub fn insert_before(&self, new_text: &str) -> Fix {
        convert_wit_fix(self.handle.insert_before(new_text))
    }

    /// Create a fix that inserts multiple lines before this directive.
    pub fn insert_before_many(&self, lines: &[&str]) -> Fix {
        let owned: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
        convert_wit_fix(self.handle.insert_before_many(&owned))
    }
}

/// A directive paired with its parent block context.
pub struct DirectiveWithContext {
    /// The directive itself.
    pub directive: Directive,
    /// Stack of parent directive names (e.g., `["http", "server"]`).
    pub parent_stack: Vec<String>,
    /// Nesting depth (0 = root level).
    pub depth: usize,
}

impl DirectiveWithContext {
    /// Get the immediate parent directive name, if any.
    pub fn parent(&self) -> Option<&str> {
        self.parent_stack.last().map(|s| s.as_str())
    }

    /// Check if this directive is inside a specific parent context.
    pub fn is_inside(&self, parent_name: &str) -> bool {
        self.parent_stack.iter().any(|p| p == parent_name)
    }

    /// Check if the immediate parent is a specific directive.
    pub fn parent_is(&self, parent_name: &str) -> bool {
        self.parent() == Some(parent_name)
    }

    /// Check if this directive is at root level.
    pub fn is_at_root(&self) -> bool {
        self.parent_stack.is_empty()
    }
}

/// A config item (directive, comment, or blank line).
pub enum ConfigItem {
    Directive(Directive),
    Comment(CommentInfo),
    BlankLine(BlankLineInfo),
}

impl ConfigItem {
    fn from_wit(item: config_api::ConfigItem) -> Self {
        match item {
            config_api::ConfigItem::DirectiveItem(d) => {
                ConfigItem::Directive(Directive::from_handle(d))
            }
            config_api::ConfigItem::CommentItem(c) => ConfigItem::Comment(CommentInfo {
                text: c.text,
                line: c.line as usize,
                column: c.column as usize,
                leading_whitespace: c.leading_whitespace,
                trailing_whitespace: c.trailing_whitespace,
                start_offset: c.start_offset as usize,
                end_offset: c.end_offset as usize,
            }),
            config_api::ConfigItem::BlankLineItem(b) => ConfigItem::BlankLine(BlankLineInfo {
                line: b.line as usize,
                content: b.content,
                start_offset: b.start_offset as usize,
            }),
        }
    }
}

/// Comment data.
pub struct CommentInfo {
    pub text: String,
    pub line: usize,
    pub column: usize,
    pub leading_whitespace: String,
    pub trailing_whitespace: String,
    pub start_offset: usize,
    pub end_offset: usize,
}

/// Blank line data.
pub struct BlankLineInfo {
    pub line: usize,
    pub content: String,
    pub start_offset: usize,
}

/// Argument data.
pub struct ArgumentInfo {
    pub value: String,
    pub raw: String,
    pub arg_type: ArgumentType,
    pub line: usize,
    pub column: usize,
    pub start_offset: usize,
    pub end_offset: usize,
}

impl ArgumentInfo {
    fn from_wit(arg: config_api::ArgumentInfo) -> Self {
        Self {
            value: arg.value,
            raw: arg.raw,
            arg_type: match arg.arg_type {
                config_api::ArgumentType::Literal => ArgumentType::Literal,
                config_api::ArgumentType::QuotedString => ArgumentType::QuotedString,
                config_api::ArgumentType::SingleQuotedString => ArgumentType::SingleQuotedString,
                config_api::ArgumentType::Variable => ArgumentType::Variable,
            },
            line: arg.line as usize,
            column: arg.column as usize,
            start_offset: arg.start_offset as usize,
            end_offset: arg.end_offset as usize,
        }
    }

    /// Get the argument's string value.
    pub fn as_str(&self) -> &str {
        &self.value
    }

    /// Check if this is a variable argument.
    pub fn is_variable(&self) -> bool {
        matches!(self.arg_type, ArgumentType::Variable)
    }

    /// Check if this is a quoted string.
    pub fn is_quoted(&self) -> bool {
        matches!(self.arg_type, ArgumentType::QuotedString)
    }

    /// Check if this is a single-quoted string.
    pub fn is_single_quoted(&self) -> bool {
        matches!(self.arg_type, ArgumentType::SingleQuotedString)
    }

    /// Check if this is a literal value.
    pub fn is_literal(&self) -> bool {
        matches!(self.arg_type, ArgumentType::Literal)
    }
}

/// Argument value type.
pub enum ArgumentType {
    Literal,
    QuotedString,
    SingleQuotedString,
    Variable,
}

/// Convert a WIT fix to an SDK fix.
fn convert_wit_fix(fix: wit_types::Fix) -> Fix {
    Fix {
        line: fix.line as usize,
        old_text: fix.old_text,
        new_text: fix.new_text,
        delete_line: fix.delete_line,
        insert_after: fix.insert_after,
        start_offset: fix.start_offset.map(|v| v as usize),
        end_offset: fix.end_offset.map(|v| v as usize),
    }
}