wdl-lint 0.26.0

Lint rules for Workflow Description Language (WDL) documents
Documentation
//! A lint rule that ensures structs are defined with pascal case names.

use convert_case::Boundary;
use convert_case::Case;
use convert_case::Converter;
use wdl_analysis::Diagnostics;
use wdl_analysis::Example;
use wdl_analysis::LabeledSnippet;
use wdl_analysis::VisitReason;
use wdl_analysis::Visitor;
use wdl_ast::AstNode;
use wdl_ast::AstToken;
use wdl_ast::Diagnostic;
use wdl_ast::Span;
use wdl_ast::SyntaxKind;
use wdl_ast::SyntaxNode;
use wdl_ast::v1::StructDefinition;

use crate::Rule;
use crate::Tag;
use crate::TagSet;

/// The identifier for the pascal case rule.
const ID: &str = "PascalCase";

/// Creates a "use pascal case" diagnostic.
fn use_pascal_case(name: &str, properly_cased_name: &str, span: Span) -> Diagnostic {
    Diagnostic::warning(format!("struct name `{name}` is not PascalCase"))
        .with_rule(ID)
        .with_label("this name must be PascalCase", span)
        .with_fix(format!("replace `{name}` with `{properly_cased_name}`"))
}

/// Detects structs defined without a pascal case name.
#[derive(Default, Debug, Clone, Copy)]
pub struct PascalCaseRule;

impl Rule for PascalCaseRule {
    fn id(&self) -> &'static str {
        ID
    }

    fn description(&self) -> &'static str {
        "Ensures that structs are defined with PascalCase names."
    }

    fn explanation(&self) -> &'static str {
        "Struct names should be in PascalCase. Maintaining a consistent naming convention makes \
         the code easier to read and understand."
    }

    fn examples(&self) -> &'static [Example] {
        &[Example {
            negative: LabeledSnippet {
                label: Some("Struct names should be in PascalCase"),
                snippet: r#"version 1.2

struct registered_user {
    String name
}
"#,
            },
            revised: Some(LabeledSnippet {
                label: None,
                snippet: r#"version 1.2

struct RegisteredUser {
    String name
}
"#,
            }),
        }]
    }

    fn tags(&self) -> TagSet {
        TagSet::new(&[Tag::Naming, Tag::Style, Tag::Clarity])
    }

    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
        Some(&[
            SyntaxKind::VersionStatementNode,
            SyntaxKind::StructDefinitionNode,
        ])
    }

    fn related_rules(&self) -> &'static [&'static str] {
        &["SnakeCase"]
    }
}

/// Checks if the given name is pascal case, and if not adds a warning to the
/// diagnostics.
fn check_name(
    name: &str,
    span: Span,
    diagnostics: &mut Diagnostics,
    node: &SyntaxNode,
    exceptable_nodes: &Option<&'static [SyntaxKind]>,
) {
    let converter = Converter::new()
        .remove_boundaries(&[Boundary::DigitLower, Boundary::LowerDigit])
        .to_case(Case::Pascal);
    let properly_cased_name = converter.convert(name);
    if name != properly_cased_name {
        diagnostics.exceptable_add(
            use_pascal_case(name, &properly_cased_name, span),
            node,
            exceptable_nodes,
        );
    }
}

impl Visitor for PascalCaseRule {
    fn reset(&mut self) {
        *self = Self;
    }

    fn struct_definition(
        &mut self,
        diagnostics: &mut Diagnostics,
        reason: VisitReason,
        def: &StructDefinition,
    ) {
        if reason == VisitReason::Exit {
            return;
        }

        let name = def.name();
        check_name(
            name.text(),
            name.span(),
            diagnostics,
            def.inner(),
            &self.exceptable_nodes(),
        );
    }
}