### Test Utilities for Ordering Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/rules/ordering.rs.html Helper functions and setup for unit testing the ordering logic. ```rust fn span(line: usize) -> Span { Span::new(line, 1, 0, 0) } ``` -------------------------------- ### Config::find_and_load Source: https://docs.rs/gdstyle/0.2.5/gdstyle/config/struct.Config.html Searches for a configuration file starting from a given directory and walking up the directory tree. ```APIDOC ## pub fn find_and_load(start_dir: &Path) -> Self ### Description Searches for a configuration file by walking up the directory tree starting from the specified directory. ### Parameters - **start_dir** (&Path) - Required - The directory to begin the search from. ### Returns - **Self** - Returns the loaded Config instance. ``` -------------------------------- ### Compute attached starts Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/formatter.rs.html Walks backward from declaration lines to identify comments and annotations that should move with the member. ```rust /// For each anchor, walk backward from its declaration line to find attached /// comments / annotations: those belong to the member and move with it. fn compute_attached_starts(merged: &[Anchor], lines: &[&str]) -> Vec { let mut attached_starts: Vec = Vec::new(); for (i, &(decl_line, _, _)) in merged.iter().enumerate() { let prev_boundary = if i > 0 { merged[i - 1].0 + 1 } else { 0 }; let mut start = decl_line; let mut j = decl_line; while j > prev_boundary { j -= 1; let content = if j < lines.len() { lines[j].trim() } else { "" }; if content.starts_with('#') || content.starts_with('@') { start = j; } else if content.is_empty() { // Blank line: keep looking only if a comment sits directly above. let above = if j > prev_boundary && j - 1 < lines.len() { lines[j - 1].trim() } else { "" }; if !above.starts_with('#') && !above.starts_with('@') { break; } } else { break; } } attached_starts.push(start); } attached_starts } ``` -------------------------------- ### pub fn line_byte_offset(lines: &[String], line_idx: usize) -> usize Source: https://docs.rs/gdstyle/0.2.5/gdstyle/diagnostic/fn.line_byte_offset.html Computes the byte offset of the start of a given line (0-indexed line_idx). ```APIDOC ## Function: line_byte_offset ### Description Compute the byte offset of the start of a given line (0-indexed line_idx). ### Signature `pub fn line_byte_offset(lines: &[String], line_idx: usize) -> usize` ### Parameters - **lines** (&[String]) - A slice of strings representing the lines of text. - **line_idx** (usize) - The 0-indexed line number to calculate the offset for. ### Returns - **usize** - The byte offset of the start of the specified line. ``` -------------------------------- ### Check Operand Start Boundary Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/rules/quality.rs.html Determines if a token marks a clean boundary for the start of an operand, preventing partial matches within larger expressions. ```rust fn is_operand_start_boundary(token: Option<&Token>) -> bool { match token { None => true, Some(t) => matches!( t.kind, TokenKind::Newline | TokenKind::Semicolon | TokenKind::Colon | TokenKind::Comma | TokenKind::LeftParen | TokenKind::LeftBracket | TokenKind::LeftBrace | TokenKind::If | TokenKind::Elif | TokenKind::While | TokenKind::When | TokenKind::Return | TokenKind::Assert ) } } ``` -------------------------------- ### Config::from_file Source: https://docs.rs/gdstyle/0.2.5/gdstyle/config/struct.Config.html Loads configuration settings from a specified TOML file path. ```APIDOC ## pub fn from_file(path: &Path) -> Result ### Description Loads configuration from a TOML file located at the provided path. ### Parameters - **path** (&Path) - Required - The filesystem path to the TOML configuration file. ### Returns - **Result** - Returns the loaded Config instance or a ConfigError if the file cannot be read or parsed. ``` -------------------------------- ### Read Annotation Token Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/lexer.rs.html Extracts annotation names starting with the @ symbol. ```rust fn read_annotation(&mut self) -> Token { let start_offset = self.byte_offset; let start_col = self.column; self.advance(); // skip @ let mut name = String::new(); while !self.is_at_end() && (self.current_char().is_ascii_alphanumeric() || self.current_char() == '_') { name.push(self.current_char()); self.advance(); } let text = format!("@{}", name); let span = Span::new(self.line, start_col, start_offset, text.len()); Token::new(TokenKind::Annotation(name), span, text) } ``` -------------------------------- ### Load Configuration from Files Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/config.rs.html Methods to load configuration from a specific file or by searching parent directories for a configuration file. ```rust impl Config { /// Load configuration from a TOML file. pub fn from_file(path: &Path) -> Result { let content = std::fs::read_to_string(path) .map_err(|e| ConfigError::ReadError(path.display().to_string(), e.to_string()))?; let config: Config = toml::from_str(&content) .map_err(|e| ConfigError::ParseError(path.display().to_string(), e.to_string()))?; Ok(config) } /// Search for a config file starting from the given directory, walking up. pub fn find_and_load(start_dir: &Path) -> Self { let config_names = ["gdstyle.toml", ".gdstyle.toml"]; let mut dir = start_dir.to_path_buf(); loop { for name in &config_names { let path = dir.join(name); if path.exists() { match Self::from_file(&path) { Ok(config) => return config, Err(e) => { eprintln!("warning: failed to load {}: {}", path.display(), e); } } } } if !dir.pop() { break; } } Self::default() } /// Rules that are off by default and must be explicitly enabled. const OFF_BY_DEFAULT: &[&str] = &[ "quality/type-hint", "quality/empty-function", "quality/no-debug-print", ]; /// Check if a rule is enabled with the given default severity. pub fn is_rule_enabled(&self, rule_name: &str) -> bool { match self.rules.get(rule_name) { Some(RuleSeverityConfig::Off) => false, Some(_) => true, None => !Self::OFF_BY_DEFAULT.contains(&rule_name), } } } ``` -------------------------------- ### Get Next Significant Token Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/fixer.rs.html Finds the next token in the stream that is not trivia. ```rust fn next_significant(tokens: &[Token], idx: usize) -> Option<&Token> { tokens .iter() .skip(idx + 1) .find(|tok| !is_trivia(&tok.kind)) } ``` -------------------------------- ### Operator Identification Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/lexer.rs.html Determines if a character marks the start of a known GDScript operator or delimiter. ```rust fn is_known_operator_start(ch: char) -> bool { matches!( ch, '-' | ',' | ';' | ':' | '!' | '.' | '(' | ')' | '[' | ']' | '{' | '}' | '*' | '/' | '&' | '%' | '^' | '+' | '<' | '=' | '>' | '|' | '~' ) } ``` -------------------------------- ### Collect GDScript files with a filter Source: https://docs.rs/gdstyle/0.2.5/gdstyle/collect/fn.collect_gdscript_files.html Demonstrates how to initialize a PathFilter and use it to collect .gd files from a project directory. ```rust use gdstyle::collect::{collect_gdscript_files, PathFilter}; use std::path::PathBuf; let filter = PathFilter::new(&["addons".into()], &["addons/my_plugin".into()]); let files = collect_gdscript_files(&[PathBuf::from("res-project")], &filter); ``` -------------------------------- ### Validate private member naming Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/rules/naming.rs.html Checks that members starting with an underscore do not have export annotations. ```rust pub fn check_private_underscore_prefix(file: &ScriptFile, diagnostics: &mut Vec) { // This rule checks that functions/vars starting with _ don't have @export. // It's more of a convention check. for member in &file.members { if let ClassMember::Variable { name, annotations, span, .. } = member { if name.starts_with('_') && annotations .iter() ``` -------------------------------- ### Test Utilities Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/rules/formatting.rs.html Helper functions for creating mock files and tokenizing source strings during unit testing. ```rust fn make_file(lines: &[&str]) -> ScriptFile { ScriptFile { path: "test.gd".to_string(), members: vec![], lines: lines.iter().map(|s| s.to_string()).collect(), } } fn tokenize(source: &str) -> Vec { let mut lexer = crate::lexer::Lexer::new(source); lexer.tokenize() } ``` -------------------------------- ### Quality Rule Test Utilities Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/rules/quality.rs.html Helper functions for setting up test environments, including span generation and source lexing. ```rust fn span(line: usize) -> Span { Span::new(line, 1, 0, 0) } /// Lex a source snippet into the token stream the rules operate on. /// Keeps dotted-path regression tests readable instead of hand-building /// `Identifier`/`Dot` sequences. fn lex(source: &str) -> Vec { crate::lexer::Lexer::new(source).tokenize() } fn empty_file() -> ScriptFile { ScriptFile { path: "test.gd".to_string(), lines: vec![], members: vec![], } } ``` -------------------------------- ### Token Stream Navigation Utilities Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/parser.rs.html Helper methods for skipping whitespace, comments, and expressions, as well as basic token stream navigation. ```rust fn skip_newlines(&mut self) { while !self.is_at_end() && self.current_kind() == TokenKind::Newline { self.advance(); } } fn skip_newlines_and_comments(&mut self) { while !self.is_at_end() { match self.current_kind() { TokenKind::Newline => self.advance(), TokenKind::Comment(_) | TokenKind::DocComment(_) => self.advance(), _ => break, } } } fn skip_to_newline(&mut self) { while !self.is_at_end() && self.current_kind() != TokenKind::Newline && self.current_kind() != TokenKind::Eof { self.advance(); } } fn skip_expression(&mut self) { let mut paren_depth = 0; while !self.is_at_end() { match self.current_kind() { TokenKind::LeftParen | TokenKind::LeftBracket | TokenKind::LeftBrace => { paren_depth += 1; self.advance(); } TokenKind::RightParen | TokenKind::RightBracket | TokenKind::RightBrace => { if paren_depth == 0 { break; } paren_depth -= 1; self.advance(); } TokenKind::Comma if paren_depth == 0 => break, TokenKind::Newline if paren_depth == 0 => break, _ => { self.advance(); } } } } fn current_token(&self) -> &Token { &self.tokens[self.position] } fn current_kind(&self) -> TokenKind { self.tokens[self.position].kind.clone() } fn advance(&mut self) { if self.position < self.tokens.len() - 1 { self.position += 1; } } fn is_at_end(&self) -> bool { self.position >= self.tokens.len() || self.tokens[self.position].kind == TokenKind::Eof } } ``` -------------------------------- ### run_all_rules_with_source Source: https://docs.rs/gdstyle/0.2.5/gdstyle/rules/fn.run_all_rules_with_source.html Executes all enabled lint rules, optionally incorporating source code for token-gap analysis. ```APIDOC ## Function: run_all_rules_with_source ### Description Runs all enabled lint rules on the provided script file. It supports optional source input for performing token-gap analysis. ### Signature `pub fn run_all_rules_with_source(file: &ScriptFile, tokens: &[Token], config: &Config, source: Option<&str>) -> Vec` ### Parameters - **file** (&ScriptFile) - The script file to be analyzed. - **tokens** (&[Token]) - A slice of tokens extracted from the script. - **config** (&Config) - The configuration settings for the lint rules. - **source** (Option<&str>) - Optional source code string used for token-gap analysis. ### Returns - **Vec** - A collection of diagnostic results generated by the lint rules. ``` -------------------------------- ### Parse Hexadecimal Numbers Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/lexer.rs.html Parses hexadecimal numeric literals starting with a prefix, stripping underscores before conversion. ```rust fn read_hex_number( &mut self, start_offset: usize, start_col: usize, mut text: String, ) -> Token { while !self.is_at_end() && (self.current_char().is_ascii_hexdigit() || self.current_char() == '_') { text.push(self.current_char()); self.advance(); } let span = Span::new(self.line, start_col, start_offset, text.len()); let hex_str = text[2..].replace('_', ""); match i64::from_str_radix(&hex_str, 16) { Ok(val) => Token::new(TokenKind::Integer(val), span, text), Err(_) => Token::new( TokenKind::Error(format!("invalid hex: {}", text)), span, text, ), } } ``` -------------------------------- ### Lexer::new Source: https://docs.rs/gdstyle/0.2.5/gdstyle/lexer/struct.Lexer.html Creates a new instance of the Lexer with the provided source code. ```APIDOC ## pub fn new(source: &'a str) -> Self ### Description Creates a new Lexer instance to process the given GDScript source string. ### Parameters - **source** (&'a str) - Required - The GDScript source code to be tokenized. ``` -------------------------------- ### Get Leading Annotation Line Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/formatter.rs.html Finds the earliest line number occupied by any annotation attached to a class member. ```rust fn leading_annotation_line(member: &ClassMember) -> Option { let annotations = match member { ClassMember::Variable { annotations, .. } | ClassMember::Function { annotations, .. } | ClassMember::StaticVariable { annotations, .. } => annotations.as_slice(), _ => return None, }; annotations.iter().map(|a| a.span.line).min() } ``` -------------------------------- ### Enforce Comment Spacing Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/rules/formatting.rs.html Ensures that comments start with a space after the '#' character, while ignoring doc comments and specific region markers. ```rust pub fn check_comment_spacing( tokens: &[Token], file: &ScriptFile, diagnostics: &mut Vec, ) { for token in tokens { match &token.kind { TokenKind::Comment(content) => { if !content.is_empty() && !content.starts_with(' ') && !content.starts_with('!') && !content.starts_with("region") && !content.starts_with("endregion") { let first_char = content.chars().next().unwrap(); if first_char.is_ascii_alphabetic() || first_char == '_' || first_char == '\t' { continue; } // Insert a space after the '#'. let insert_offset = token.span.offset + 1; // after '#' diagnostics.push( Diagnostic::warning( "format/comment-spacing", "add a space after '#' in comments".to_string(), token.span, &file.path, ) .with_fix(Fix { replacements: vec![Replacement { offset: insert_offset, length: 0, new_text: " ".to_string(), }], is_safe: true, }), ); } } TokenKind::DocComment(_) => { // Don't touch ## doc comments. ## is meaningful in GDScript, and // codebases sometimes use ## as a "double-comment-out" of code // (e.g. `##var foo`) where a blanket "add a space after ##" rewrite // changes how a reader interprets the line. } _ => {} } } } ``` -------------------------------- ### pub fn run_all_rules Source: https://docs.rs/gdstyle/0.2.5/gdstyle/rules/fn.run_all_rules.html Executes all enabled lint rules on a parsed script file. ```APIDOC ## run_all_rules ### Description Runs all enabled lint rules on a parsed script file. ### Signature `pub fn run_all_rules(file: &ScriptFile, tokens: &[Token], config: &Config) -> Vec` ### Parameters - **file** (&ScriptFile) - The parsed script file to analyze. - **tokens** (&[Token]) - The slice of tokens associated with the script. - **config** (&Config) - The configuration settings for the lint rules. ### Returns - **Vec** - A vector containing the diagnostics generated by the lint rules. ``` -------------------------------- ### Normalize Member Spacing Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/formatter.rs.html Rewrites source code to match canonical Godot style guide spacing between top-level class members. ```rust fn normalize_member_spacing(source: &str) -> String { let mut lexer = Lexer::new(source); let tokens = lexer.tokenize(); let members = Parser::new(&tokens).parse(); if members.is_empty() { return source.to_string(); } let lines: Vec<&str> = source.split('\n').collect(); // Walk parsed members and group adjacent `##` doc-comment lines with the // next non-doc declaration (the doc "attaches" to it). A blank line // between two doc blocks splits them: the older block becomes a // standalone class-level doc unit, the newer block is still pending. let mut units: Vec = Vec::new(); let mut pending_doc_start: Option = None; let mut pending_doc_last: Option = None; // A contiguous block of plain `#` comments awaiting the next declaration. // Unlike doc comments it never becomes its own unit; it only ever gets // absorbed as a leading comment when it sits tight against a declaration. let mut pending_comment_start: Option = None; let mut pending_comment_last: Option = None; let flush_standalone_docs = |units: &mut Vec, start: &mut Option, last: &mut Option| { if let (Some(s), Some(_)) = (*start, *last) { units.push(MemberUnit { category: 3, start: s, decl_start: s, }); } *start = None; *last = None; }; for member in &members { match member { ``` -------------------------------- ### Test Configuration Defaults Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/config.rs.html Verifies that the default configuration is initialized with expected values. ```rust #[test] fn default_config() { let config = Config::default(); assert_eq!(config.max_line_length, 100); assert!(config.use_tabs); assert_eq!(config.max_function_length, 50); assert!(config.exclude.contains(&".godot".to_string())); assert!(config.exclude.contains(&"addons".to_string())); assert!(config.include.is_empty()); } ``` -------------------------------- ### Parse RHS Identifier Chains Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/rules/quality.rs.html Extracts a sequence of identifiers starting from a given token index, supporting keywords like self and super. ```rust fn rhs_chain_starting_at(tokens: &[Token], start: usize) -> (Vec, usize) { let mut chain: Vec = Vec::new(); let mut i = start; // First segment can be a keyword (`self`/`super`) or an identifier. match tokens.get(i).map(|t| &t.kind) { Some(TokenKind::Identifier(name)) => { chain.push(name.clone()); } Some(TokenKind::Self_) => { chain.push("self".to_string()); } Some(TokenKind::Super) => { chain.push("super".to_string()); } _ => return (chain, i), } // Subsequent segments must be `.identifier`. while i + 2 < tokens.len() && tokens[i + 1].kind == TokenKind::Dot && matches!(tokens[i + 2].kind, TokenKind::Identifier(_)) { if let TokenKind::Identifier(name) = &tokens[i + 2].kind { chain.push(name.clone()); } i += 2; } (chain, i + 1) } ``` -------------------------------- ### Function signature for run_all_rules_with_source Source: https://docs.rs/gdstyle/0.2.5/gdstyle/rules/fn.run_all_rules_with_source.html The function signature defines the required parameters including the script file, tokens, configuration, and optional source string. ```rust pub fn run_all_rules_with_source( file: &ScriptFile, tokens: &[Token], config: &Config, source: Option<&str>, ) -> Vec ``` -------------------------------- ### Implement Default Configuration Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/config.rs.html Provides default values for all configuration fields, including standard exclusions. ```rust impl Default for Config { fn default() -> Self { Self { max_line_length: 100, use_tabs: true, max_function_length: 50, max_file_length: 1000, max_parameters: 5, max_returns: 6, max_nesting_depth: 4, max_local_variables: 10, max_branches: 8, max_class_variables: 15, max_public_methods: 20, max_inner_classes: 5, exclude: vec![".godot".to_string(), "addons".to_string()], include: Vec::new(), rules: HashMap::new(), } } } ``` -------------------------------- ### Format a file on disk Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/formatter.rs.html Reads a file from the provided path, formats its content, and writes it back if changes were detected. ```rust pub fn format_file(path: &std::path::Path, config: &Config) -> Result { let source = std::fs::read_to_string(path) .map_err(|e| format!("cannot read {}: {}", path.display(), e))?; let formatted = format_source(&source, config); if formatted == source { return Ok(false); } std::fs::write(path, &formatted) .map_err(|e| format!("cannot write {}: {}", path.display(), e))?; Ok(true) } ``` -------------------------------- ### Normalize Float Literals Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/formatter.rs.html Standardizes float literals by adding a leading zero to values starting with a dot or a trailing zero to values ending with a dot. ```rust fn normalize_float_literals(source: &str) -> String { let mut lexer = crate::lexer::Lexer::new(source); let tokens = lexer.tokenize(); let mut replacements: Vec<(usize, usize, String)> = Vec::new(); for token in &tokens { if let crate::token::TokenKind::Float(_) = &token.kind { let text = &token.text; if text.starts_with('.') { replacements.push((token.span.offset, token.span.length, format!("0{}", text))); } else if text.ends_with('.') { replacements.push((token.span.offset, token.span.length, format!("{}0", text))); } } } if replacements.is_empty() { return source.to_string(); } let mut result = source.to_string(); for (offset, length, new_text) in replacements.into_iter().rev() { let end = (offset + length).min(result.len()); result.replace_range(offset..end, &new_text); } result } ``` -------------------------------- ### Parser::new Source: https://docs.rs/gdstyle/0.2.5/gdstyle/parser/struct.Parser.html Creates a new Parser instance from a slice of tokens. ```APIDOC ## pub fn new(tokens: &'t [Token]) -> Self ### Description Creates a new Parser instance that borrows the provided slice of tokens. The parser does not clone the token buffer, allowing for efficient memory usage. ### Parameters - **tokens** (&'t [Token]) - Required - A reference to a slice of tokens to be parsed. ``` -------------------------------- ### Validate GDScript Class Member Order Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/rules/ordering.rs.html The primary entry point for checking if class members follow the canonical order defined in the GDScript style guide. ```rust pub fn check_class_member_order(file: &ScriptFile, diagnostics: &mut Vec) { // Members carrying a `# gdstyle:ignore=order/class-member-order` directive // opt out of the ordering check. They are skipped entirely — neither // flagged nor counted towards the running "highest category seen" — so a // pinned member (e.g. a const kept next to the enum it mirrors) does not // make its neighbours look out of order. This mirrors how the formatter's // reorder pass pins the same members in place. let suppressions = Suppressions::parse(&file.lines.join("\n")); check_member_order_recursive(&file.members, &file.path, &suppressions, diagnostics); } ``` -------------------------------- ### fn all_rules Source: https://docs.rs/gdstyle/0.2.5/gdstyle/rules/fn.all_rules.html Retrieves a static slice of tuples containing all available lint rule names and their corresponding human-readable descriptions. ```APIDOC ## fn all_rules ### Description Returns a list of all available rule names. Every lint rule is paired with a one-line human description. This function acts as the single source of truth for rule definitions. ### Signature `pub fn all_rules() -> &'static [(&'static str, &'static str)]` ### Returns - **&'static [(&'static str, &'static str)]**: A static slice of tuples where the first element is the rule name and the second element is the rule description. ``` -------------------------------- ### Lexer implementation methods Source: https://docs.rs/gdstyle/0.2.5/gdstyle/lexer/struct.Lexer.html Methods available on the Lexer struct for creating a new instance and tokenizing source code. ```rust pub fn new(source: &'a str) -> Self ``` ```rust pub fn tokenize(&mut self) -> Vec ``` -------------------------------- ### Retrieve all rule names Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/rules/mod.rs.html Returns a vector containing the names of all defined quality rules. ```rust pub fn all_rule_names() -> Vec<&'static str> { all_rules().iter().map(|(name, _)| *name).collect() } ``` -------------------------------- ### Function Signature for lint_source Source: https://docs.rs/gdstyle/0.2.5/gdstyle/linter/fn.lint_source.html The signature defines the required parameters: the source string, the file path, and the configuration object. ```rust pub fn lint_source( source: &str, file_path: &str, config: &Config, ) -> Vec ``` -------------------------------- ### pub fn lint_file(path: &Path, config: &Config) -> Result, String> Source: https://docs.rs/gdstyle/0.2.5/gdstyle/linter/fn.lint_file.html Lint a file from disk using the provided path and configuration. ```APIDOC ## lint_file ### Description Lint a file from disk. ### Signature `pub fn lint_file(path: &Path, config: &Config) -> Result, String>` ### Parameters - **path** (&Path) - The path to the file to be linted. - **config** (&Config) - The configuration settings for the linter. ### Returns - **Result, String>** - Returns a vector of diagnostics if successful, or an error string if the operation fails. ``` -------------------------------- ### Diagnostic Reporting Implementation Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/reporter.rs.html Contains the core logic for formatting diagnostics as text, JSON, or summary reports. ```rust 1use crate::diagnostic::{Diagnostic, Severity}; 2use colored::Colorize; 3 4/// Output format for lint results. 5#[derive(Debug, Clone, Copy, PartialEq, Eq)] 6pub enum OutputFormat { 7 Text, 8 Json, 9} 10 11/// Format diagnostics for human-readable terminal output. 12/// 13/// Each diagnostic prints `line:col severity message [rule]` followed by the 14/// offending source line and a caret underline (like rustc / ruff), so the 15/// reader doesn't have to jump back to an editor. The source line is read 16/// lazily from disk per file; if the file can't be read (e.g. linting an 17/// in-memory string) the caret is simply omitted. 18pub fn format_text(diagnostics: &[Diagnostic]) -> String { 19 let mut output = String::new(); 20 let mut current_file = String::new(); 21 let mut current_lines: Vec = Vec::new(); 22 23 for diag in diagnostics { 24 if diag.file != current_file { 25 if !current_file.is_empty() { 26 output.push('\n'); 27 } 28 current_file = diag.file.clone(); 29 output.push_str(¤t_file.bold().to_string()); 30 output.push('\n'); 31 current_lines = std::fs::read_to_string(¤t_file) 32 .map(|s| s.lines().map(str::to_string).collect()) 33 .unwrap_or_default(); 34 } 35 36 let severity_str = match diag.severity { 37 Severity::Warning => "warning".yellow().to_string(), 38 Severity::Error => "error".red().to_string(), 39 }; 40 41 let location = format!(" {}:{}", diag.span.line, diag.span.column) 42 .dimmed() 43 .to_string(); 44 let rule = format!("[{}]", diag.rule).dimmed().to_string(); 45 46 output.push_str(&format!( 47 "{} {} {} {}\n", 48 location, severity_str, diag.message, rule 49 )); 50 51 // Source line + caret underline, when we have the source and a 52 // real (1-based) column. 53 if let Some(src_line) = current_lines.get(diag.span.line.wrapping_sub(1)) { 54 if diag.span.column >= 1 { 55 output.push_str(&format!(" {}\n", src_line.dimmed())); 56 let caret_pad = caret_padding(src_line, diag.span.column - 1); 57 output.push_str(&format!(" {}{}\n", caret_pad, "^".cyan())); 58 } 59 } 60 } 61 62 output 63} 64 65/// Build the indentation that places a caret under column `col` (0-based) of 66/// `line`, preserving tabs so the caret lines up in a tab-rendering terminal. 67fn caret_padding(line: &str, col: usize) -> String { 68 line.chars() 69 .take(col) 70 .map(|c| if c == '\t' { '\t' } else { ' ' }) 71 .collect() 72} 73 74/// Format diagnostics as JSON. 75/// 76/// Serialization of these plain `#[derive(Serialize)]` structs cannot fail in 77/// practice, so we `expect` rather than swallowing the error into "[]": 78/// emitting an empty array on failure would make a CI pipeline read a broken 79/// run as "no issues" and pass. 80pub fn format_json(diagnostics: &[Diagnostic]) -> String { 81 serde_json::to_string_pretty(diagnostics).expect("diagnostics serialize to JSON") 82} 83 84/// Print a summary of lint results. 85pub fn format_summary(diagnostics: &[Diagnostic], file_count: usize) -> String { 86 let warning_count = diagnostics 87 .iter() 88 .filter(|d| d.severity == Severity::Warning) 89 .count(); 90 let error_count = diagnostics 91 .iter() 92 .filter(|d| d.severity == Severity::Error) 93 .count(); 94 95 if diagnostics.is_empty() { 96 format!( 97 "\n{} {} {} checked, no issues found.", 98 "✓".green(), 99 file_count, 100 pluralize(file_count, "file", "files") 101 ) 102 } else { 103 let mut parts = Vec::new(); 104 if error_count > 0 { 105 parts.push( 106 format( 107 "{} {}", 108 error_count, 109 pluralize(error_count, "error", "errors") 110 ) 111 .red() 112 .to_string(), 113 ); 114 } 115 if warning_count > 0 { 116 parts.push( 117 format( 118 "{} {}", 119 warning_count, 120 pluralize(warning_count, "warning", "warnings") 121 ) 122 .yellow() 123 .to_string(), 124 ); 125 } 126 127 format( 128 "\n{} {} checked, {} found.", 129 file_count, 130 pluralize(file_count, "file", "files"), 131 parts.join(", ") 132 ) 133 } 134} ``` -------------------------------- ### all_rules() Source: https://docs.rs/gdstyle/0.2.5/gdstyle/rules/index.html Returns a list of all available rule names, paired with a one-line human description. This function serves as the single source of truth for rule definitions. ```APIDOC ## all_rules() ### Description Returns a list of all available rule names. Every lint rule is paired with a one-line human description. This is the single source of truth for rule definitions. ### Signature `pub fn all_rules() -> Vec<(String, String)>` ``` -------------------------------- ### Lexer Navigation and Whitespace Methods Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/lexer.rs.html Methods for managing lexer state, including character advancement and whitespace skipping logic. ```rust matches!(self.peek_char(), Some('"') | Some('\'')) } fn is_at_end(&self) -> bool { self.position >= self.chars.len() } fn advance(&mut self) { if self.position < self.chars.len() { self.byte_offset += self.chars[self.position].len_utf8(); } self.position += 1; self.column += 1; } /// Skip all leading whitespace (tabs and spaces) on a line inside brackets. fn skip_line_whitespace(&mut self) { while !self.is_at_end() && (self.current_char() == ' ' || self.current_char() == '\t') { self.advance(); } } /// Skip mid-line whitespace. /// /// Spaces and tabs are consumed in ONE interleaved pass. Two sequential /// passes (all spaces, then all tabs) silently stranded anything after the /// first tab: a `\` continuation line indented `\t ` — a tab for block /// depth plus spaces to align under the opening expression — left the /// lexer sitting on a space it had already stopped skipping, which /// surfaced as `unexpected character: ' '` on valid GDScript. fn skip_spaces(&mut self) { while !self.is_at_end() { match self.current_char() { ' ' => self.advance(), // At line start a tab is indentation and belongs to // process_indentation; mid-line it is just spacing. '\t' if !self.at_line_start => self.advance(), _ => return, } } } fn current_span(&self, length: usize) -> Span { Span::new(self.line, self.column, self.byte_offset, length) } } ``` -------------------------------- ### Test String Case Conversion Utilities Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/rules/naming.rs.html Verifies correct transformation of strings into snake_case and SCREAMING_SNAKE_CASE, including handling of acronyms and digit transitions. ```rust #[test] fn test_to_snake_case() { assert_eq!(to_snake_case("PlayerSpeed"), "player_speed"); assert_eq!(to_snake_case("StateMachine"), "state_machine"); assert_eq!(to_snake_case("_PrivateVar"), "_private_var"); // S7: acronym handling assert_eq!(to_snake_case("AIInControl"), "ai_in_control"); assert_eq!(to_snake_case("HTTPRequest"), "http_request"); assert_eq!(to_snake_case("UIElement"), "ui_element"); assert_eq!(to_snake_case("IOHandler"), "io_handler"); assert_eq!(to_snake_case("GPUCompute"), "gpu_compute"); assert_eq!(to_snake_case("SSEEvent"), "sse_event"); assert_eq!(to_snake_case("HTMLParser"), "html_parser"); assert_eq!(to_snake_case("APIKey"), "api_key"); // Non-acronym cases still work assert_eq!(to_snake_case("PlayerInControl"), "player_in_control"); assert_eq!(to_snake_case("None"), "none"); // Digit-to-uppercase transitions assert_eq!(to_snake_case("Vector2D"), "vector_2d"); assert_eq!(to_snake_case("Area2D"), "area_2d"); assert_eq!(to_snake_case("Item3D"), "item_3d"); assert_eq!(to_snake_case("Sprite2D"), "sprite_2d"); assert_eq!(to_snake_case("Node2D"), "node_2d"); // Digit in middle gets underscore before digit group assert_eq!(to_snake_case("player1speed"), "player_1speed"); } ``` ```rust #[test] fn test_to_screaming_snake_case() { assert_eq!(to_screaming_snake_case("maxSpeed"), "MAX_SPEED"); assert_eq!(to_screaming_snake_case("PlayerState"), "PLAYER_STATE"); // S7: acronym handling assert_eq!(to_screaming_snake_case("AIInControl"), "AI_IN_CONTROL"); assert_eq!(to_screaming_snake_case("HTTPRequest"), "HTTP_REQUEST"); assert_eq!(to_screaming_snake_case("UIElement"), "UI_ELEMENT"); assert_eq!(to_screaming_snake_case("SSEEvent"), "SSE_EVENT"); assert_eq!( to_screaming_snake_case("PlayerInControl"), "PLAYER_IN_CONTROL" ); assert_eq!(to_screaming_snake_case("None"), "NONE"); } ``` -------------------------------- ### format_summary Source: https://docs.rs/gdstyle/0.2.5/gdstyle/reporter/index.html Prints a summary of the lint results to the output. ```APIDOC ## format_summary ### Description Print a summary of lint results. ### Signature `pub fn format_summary(diagnostics: &[Diagnostic])` ``` -------------------------------- ### pub fn check_await_in_loop(file: &ScriptFile, diagnostics: &mut Vec) Source: https://docs.rs/gdstyle/0.2.5/gdstyle/rules/quality/fn.check_await_in_loop.html Analyzes a ScriptFile for await expressions inside loops and appends any found issues to the provided diagnostics vector. ```APIDOC ## Function: check_await_in_loop ### Description Warns when `await` is used inside a `for` or `while` loop within the provided script file. ### Signature `pub fn check_await_in_loop(file: &ScriptFile, diagnostics: &mut Vec)` ### Parameters - **file** (&ScriptFile) - The script file to analyze. - **diagnostics** (&mut Vec) - A mutable reference to a vector where detected issues will be stored. ``` -------------------------------- ### Apply Deletion Fix Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/fixer.rs.html Demonstrates applying a deletion fix to a source string. ```rust fn apply_deletion() { let source = "var x = 5 \n"; let diags = vec![make_diag(9, 3, "", true)]; let result = apply_fixes(source, &diags, true); assert_eq!(result, "var x = 5\n"); } ``` -------------------------------- ### Lexer Helper Methods Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/lexer.rs.html Utility methods for reading characters and peeking ahead in the input stream. ```rust 894 fn read_single_char(&mut self, kind: TokenKind) -> Token { 895 let span = self.current_span(1); 896 let text = self.current_char().to_string(); 897 self.advance(); 898 Token::new(kind, span, text) 899 } 900 901 // Helper methods. 902 903 fn current_char(&self) -> char { 904 self.chars[self.position] 905 } 906 907 fn peek_char(&self) -> Option { 908 self.chars.get(self.position + 1).copied() 909 } 910 911 fn peek_char_at(&self, offset: usize) -> Option { 912 self.chars.get(self.position + offset).copied() 913 } 914 915 fn peek_is_quote(&self) -> bool { ``` -------------------------------- ### Execute Lint Rules Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/rules/mod.rs.html Conditional execution of various formatting, ordering, and quality rules based on the provided configuration. ```rust if let Some(src) = source { formatting::check_brace_spacing(tokens, file, src, &mut diagnostics); } } if config.is_rule_enabled("format/call-paren-spacing") { if let Some(src) = source { formatting::check_call_paren_spacing(tokens, file, src, &mut diagnostics); } } if config.is_rule_enabled("format/float-literal-zeros") { formatting::check_float_literal_zeros(tokens, file, &mut diagnostics); } if config.is_rule_enabled("format/large-number-underscores") { formatting::check_large_number_underscores(tokens, file, &mut diagnostics); } if config.is_rule_enabled("format/enum-one-per-line") { formatting::check_enum_one_per_line(file, source, &mut diagnostics); } // Ordering rules. if config.is_rule_enabled("order/class-member-order") { ordering::check_class_member_order(file, &mut diagnostics); } // Quality rules. if config.is_rule_enabled("quality/max-function-length") { quality::check_max_function_length(file, config, &mut diagnostics); } if config.is_rule_enabled("quality/max-file-length") { quality::check_max_file_length(file, config, &mut diagnostics); } if config.is_rule_enabled("quality/max-parameters") { quality::check_max_parameters(file, config, &mut diagnostics); } if config.is_rule_enabled("quality/unnecessary-pass") { quality::check_unnecessary_pass_in_functions(file, &mut diagnostics); } if config.is_rule_enabled("quality/no-debug-print") { quality::check_no_debug_print(tokens, file, &mut diagnostics); } if config.is_rule_enabled("quality/self-comparison") { quality::check_self_comparison(tokens, file, &mut diagnostics); } if config.is_rule_enabled("quality/no-self-assign") { quality::check_no_self_assign(tokens, file, &mut diagnostics); } if config.is_rule_enabled("quality/duplicate-dict-key") { quality::check_duplicate_dict_key(tokens, file, &mut diagnostics); } if config.is_rule_enabled("quality/duplicated-load") { quality::check_duplicated_load(tokens, file, &mut diagnostics); } if config.is_rule_enabled("quality/type-hint") { quality::check_type_hint(file, &mut diagnostics); } if config.is_rule_enabled("quality/empty-function") { quality::check_empty_function(file, &mut diagnostics); } if config.is_rule_enabled("quality/max-class-variables") { quality::check_max_class_variables(file, config, &mut diagnostics); } if config.is_rule_enabled("quality/max-public-methods") { quality::check_max_public_methods(file, config, &mut diagnostics); } if config.is_rule_enabled("quality/max-inner-classes") { quality::check_max_inner_classes(file, config, &mut diagnostics); } if config.is_rule_enabled("quality/no-else-return") { quality::check_no_else_return(file, &mut diagnostics); } if config.is_rule_enabled("quality/unreachable-code") { quality::check_unreachable_code(file, &mut diagnostics); } if config.is_rule_enabled("quality/await-in-loop") { quality::check_await_in_loop(file, &mut diagnostics); } if config.is_rule_enabled("quality/allocation-in-loop") { quality::check_allocation_in_loop(file, &mut diagnostics); } if config.is_rule_enabled("quality/process-get-node") { quality::check_process_get_node(file, &mut diagnostics); } if config.is_rule_enabled("quality/max-nesting-depth") { quality::check_max_nesting_depth(file, config, &mut diagnostics); } if config.is_rule_enabled("quality/max-returns") { quality::check_max_returns(file, config, &mut diagnostics); } if config.is_rule_enabled("quality/max-branches") { quality::check_max_branches(file, config, &mut diagnostics); } if config.is_rule_enabled("quality/max-local-variables") { quality::check_max_local_variables(file, config, &mut diagnostics); } // Sort diagnostics by line number. diagnostics.sort_by_key(|d| (d.span.line, d.span.column)); diagnostics } ``` -------------------------------- ### all_rule_names function signature Source: https://docs.rs/gdstyle/0.2.5/gdstyle/rules/fn.all_rule_names.html Displays the function signature for retrieving all rule names. ```rust pub fn all_rule_names() -> Vec<&'static str> ``` -------------------------------- ### Test Special Syntax and Operators Source: https://docs.rs/gdstyle/0.2.5/src/gdstyle/lexer.rs.html Verifies tokenization of node paths, boolean operators, and raw strings. ```rust #[test] fn dollar_sign_node_path() { let kinds = token_kinds("$Label"); assert_eq!( kinds, vec![ TokenKind::Dollar, TokenKind::Identifier("Label".to_string()), ] ); } ``` ```rust #[test] fn boolean_operators() { let kinds = token_kinds("and or not"); assert_eq!(kinds, vec![TokenKind::And, TokenKind::Or, TokenKind::Not]); } ``` ```rust #[test] fn raw_string() { let kinds = token_kinds("r\"hello\\nworld\""); assert!( matches!(kinds[0], TokenKind::String(ref s) if s.prefix == StringPrefix::Raw && s.value == "hello\\nworld") ); } ``` -------------------------------- ### to_pascal_case Source: https://docs.rs/gdstyle/0.2.5/gdstyle/rules/naming/fn.to_pascal_case.html Converts a string to PascalCase. ```APIDOC ## Function: to_pascal_case ### Description Converts a provided string slice into PascalCase format. ### Signature `pub fn to_pascal_case(name: &str) -> String` ### Parameters - **name** (&str) - The input string to be converted. ### Returns - **String** - The converted string in PascalCase. ``` -------------------------------- ### format_file function signature Source: https://docs.rs/gdstyle/0.2.5/gdstyle/formatter/fn.format_file.html The function signature for formatting a file on disk. ```rust pub fn format_file(path: &Path, config: &Config) -> Result ```