#![deny(unsafe_code)]
#![deny(unreachable_pub)] #![warn(rust_2018_idioms)]
#![warn(missing_docs)]
#![warn(clippy::all)]
#![allow(
// Core allows for parser/lexer code
clippy::too_many_lines,
clippy::module_name_repetitions,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap,
clippy::must_use_candidate,
clippy::missing_errors_doc,
clippy::missing_panics_doc,
// Parser-specific patterns that are fine
clippy::wildcard_imports,
clippy::enum_glob_use,
clippy::match_same_arms,
clippy::if_not_else,
clippy::struct_excessive_bools,
clippy::items_after_statements,
clippy::return_self_not_must_use,
clippy::unused_self,
clippy::collapsible_match,
clippy::collapsible_if,
clippy::only_used_in_recursion,
clippy::items_after_test_module,
clippy::while_let_loop,
clippy::single_range_in_vec_init,
clippy::arc_with_non_send_sync,
clippy::needless_range_loop,
clippy::result_large_err,
clippy::if_same_then_else,
clippy::should_implement_trait,
clippy::manual_flatten,
// String handling in parsers
clippy::needless_raw_string_hashes,
clippy::single_char_pattern,
clippy::uninlined_format_args
)]
pub mod engine;
pub use engine::{error, parser, position};
pub use core::{Node, NodeKind, ParseError, ParseOutput, ParseResult, Parser, SourceLocation};
pub use engine::ast;
pub use engine::ast_v2;
pub use engine::edit;
pub use engine::heredoc_collector;
pub use engine::parser_context;
pub use engine::pragma_tracker;
pub use engine::quote_parser;
#[cfg(not(target_arch = "wasm32"))]
pub use error::classifier as error_classifier;
pub use error::recovery as error_recovery;
pub use perl_parser_core::util;
pub use perl_parser_core::line_index;
pub use position::{LineEnding, PositionMapper};
pub mod analysis;
pub mod builtins;
pub mod core;
#[cfg(feature = "incremental")]
pub mod incremental;
pub mod prelude;
pub mod refactor;
pub mod tdd;
pub mod tokens;
pub mod workspace;
pub mod compat;
pub mod ast_utils;
#[allow(missing_docs)]
pub mod heredoc_anti_patterns;
pub use perl_parser_core::path_normalize;
pub use perl_parser_core::path_security;
pub use perl_parser_core::percentile;
pub use perl_parser_core::qualified_name;
pub use perl_parser_core::source_file;
pub use perl_parser_core::text_line;
pub use analysis::declaration;
#[cfg(not(target_arch = "wasm32"))]
pub use analysis::index;
pub use analysis::scope_analyzer;
pub use analysis::semantic;
pub use analysis::symbol;
pub use analysis::type_inference;
pub use builtins::builtin_signatures;
pub use builtins::builtin_signatures_phf;
#[cfg(not(target_arch = "wasm32"))]
pub mod dead_code;
#[cfg(not(target_arch = "wasm32"))]
pub use dead_code as dead_code_detector;
pub use refactor::import_optimizer;
pub use refactor::modernize;
pub use refactor::modernize_refactored;
pub use refactor::refactoring;
pub use tokens::token_stream;
pub use tokens::token_wrapper;
pub use tokens::trivia;
pub use tokens::trivia_parser;
#[cfg(feature = "incremental")]
pub use incremental::incremental_advanced_reuse;
#[cfg(feature = "incremental")]
pub use incremental::incremental_checkpoint;
#[cfg(feature = "incremental")]
pub use incremental::incremental_document;
#[cfg(feature = "incremental")]
pub use incremental::incremental_edit;
#[cfg(feature = "incremental")]
#[deprecated(note = "LSP server moved to perl-lsp; perl-parser no longer handles didChange")]
pub use incremental::incremental_handler_v2;
#[cfg(feature = "incremental")]
pub use incremental::incremental_integration;
#[cfg(feature = "incremental")]
pub use incremental::incremental_simple;
#[cfg(feature = "incremental")]
pub use incremental::incremental_v2;
pub use tdd::tdd_basic;
#[cfg(test)]
pub use tdd::tdd_workflow;
pub use tdd::test_generator;
pub use tdd::test_runner;
pub use workspace::document_store;
pub use workspace::workspace_index;
#[cfg(not(target_arch = "wasm32"))]
pub use workspace::workspace_refactor;
pub use workspace::workspace_rename;
pub use error::{RecoverySalvageClass, RecoverySalvageProfile};
#[cfg(feature = "incremental")]
pub use incremental_checkpoint::{CheckpointedIncrementalParser, SimpleEdit};
pub use pragma_tracker::{PragmaState, PragmaTracker};
pub use token_stream::{Token, TokenKind, TokenStream};
pub use trivia::{NodeWithTrivia, Trivia, TriviaToken};
pub use trivia_parser::{TriviaPreservingParser, format_with_trivia};
#[cfg(feature = "incremental")]
pub use incremental::{Edit, IncrementalState, apply_edits};
pub use semantic::{
HoverInfo, SemanticAnalyzer, SemanticModel, SemanticToken, SemanticTokenModifier,
SemanticTokenType,
};
pub use symbol::{Symbol, SymbolExtractor, SymbolKind, SymbolReference, SymbolTable};
pub use import_optimizer::{
DuplicateImport, ImportAnalysis, ImportEntry, ImportOptimizer, MissingImport,
OrganizationSuggestion, SuggestionPriority, UnusedImport,
};
pub use scope_analyzer::{IssueKind, ScopeAnalyzer, ScopeIssue};
#[cfg(test)]
pub use test_generator::{
CoverageReport, Priority, RefactoringCategory, RefactoringSuggester, RefactoringSuggestion,
TestCase, TestFramework, TestGenerator, TestGeneratorOptions, TestResults, TestRunner,
};
pub use type_inference::{
PerlType, ScalarType, TypeBasedCompletion, TypeConstraint, TypeEnvironment,
TypeInferenceEngine, TypeLocation,
};
pub use refactoring::{
ModernizationPattern, RefactoringConfig, RefactoringEngine, RefactoringOperation,
RefactoringResult, RefactoringScope, RefactoringType,
};
#[cfg(test)]
pub use tdd_workflow::{
AnnotationSeverity, CoverageAnnotation, TddAction, TddConfig, TddCycleResult, TddWorkflow,
TestType, WorkflowState, WorkflowStatus,
};
#[cfg(test)]
mod tests {
use super::*;
use perl_tdd_support::must;
#[test]
fn test_basic_parsing() {
let mut parser = Parser::new("my $x = 42;");
let result = parser.parse();
assert!(result.is_ok());
let ast = must(result);
assert!(matches!(ast.kind, NodeKind::Program { .. }));
}
#[test]
fn test_variable_declaration() {
let cases = vec![
("my $x;", "my"),
("our $y;", "our"),
("local $z;", "local"),
("state $w;", "state"),
];
for (code, declarator) in cases {
let mut parser = Parser::new(code);
let result = parser.parse();
assert!(result.is_ok(), "Failed to parse: {}", code);
let ast = must(result);
if let NodeKind::Program { statements } = &ast.kind {
assert_eq!(statements.len(), 1);
let is_var_decl =
matches!(statements[0].kind, NodeKind::VariableDeclaration { .. });
assert!(is_var_decl, "Expected VariableDeclaration for: {}", code);
if let NodeKind::VariableDeclaration { declarator: decl, .. } = &statements[0].kind
{
assert_eq!(decl, declarator);
}
}
}
}
#[test]
fn test_operators() {
let cases = vec![
("$a + $b", "+"),
("$a - $b", "-"),
("$a * $b", "*"),
("$a . $b", "."),
("$a && $b", "&&"),
("$a || $b", "||"),
];
for (code, expected_op) in cases {
let mut parser = Parser::new(code);
let result = parser.parse();
assert!(result.is_ok(), "Failed to parse: {}", code);
let ast = must(result);
if let NodeKind::Program { statements } = &ast.kind {
assert!(!statements.is_empty(), "No statements found in AST for: {}", code);
let binary_node = match &statements[0].kind {
NodeKind::ExpressionStatement { expression } => match &expression.kind {
NodeKind::Binary { op, left, right } => Some((op, left, right)),
_ => None,
},
NodeKind::Binary { op, left, right } => Some((op, left, right)),
_ => None,
};
assert!(
binary_node.is_some(),
"Expected Binary operator for: {}. Found: {:?}",
code,
statements[0].kind
);
if let Some((op, left, right)) = binary_node {
assert_eq!(op, expected_op, "Operator mismatch for: {}", code);
println!("Parsing: {}", code);
println!("Left node: {:?}", left);
println!("Right node: {:?}", right);
}
}
assert!(
matches!(ast.kind, NodeKind::Program { .. }),
"Expected Program node, found: {:?}",
ast.kind
);
}
}
#[test]
fn test_operators_with_context() {
let cases: Vec<(&str, &str)> = vec![
("2 / 3", "/"), ("$a % $b", "%"), ("$a ** $b", "**"), ("$a // $b", "//"), ];
for (code, expected_op) in cases {
let mut parser = Parser::new(code);
let result = parser.parse();
assert!(result.is_ok(), "Failed to parse: {}", code);
let ast = must(result);
if let NodeKind::Program { statements } = &ast.kind {
assert!(!statements.is_empty(), "No statements found in AST for: {}", code);
let binary_node = match &statements[0].kind {
NodeKind::ExpressionStatement { expression } => match &expression.kind {
NodeKind::Binary { op, .. } => Some(op),
_ => None,
},
NodeKind::Binary { op, .. } => Some(op),
_ => None,
};
assert!(
binary_node.is_some(),
"Expected Binary operator for: {}. Found: {:?}",
code,
statements[0].kind
);
if let Some(op) = binary_node {
assert_eq!(op, expected_op, "Operator mismatch for: {}", code);
}
}
assert!(
matches!(ast.kind, NodeKind::Program { .. }),
"Expected Program node, found: {:?}",
ast.kind
);
}
}
#[test]
fn test_string_literals() {
let cases = vec![r#""hello""#, r#"'world'"#, r#"qq{foo}"#, r#"q{bar}"#];
for code in cases {
let mut parser = Parser::new(code);
let result = parser.parse();
assert!(result.is_ok(), "Failed to parse: {}", code);
}
}
#[test]
fn test_arrays_and_hashes() {
let cases = vec![
"@array",
"%hash",
"$array[0]",
"$hash{key}",
"@array[1, 2, 3]",
"@hash{'a', 'b'}",
];
for code in cases {
let mut parser = Parser::new(code);
let result = parser.parse();
assert!(result.is_ok(), "Failed to parse: {}", code);
}
}
#[test]
fn test_subroutines() {
let cases = vec![
"sub foo { }",
"sub bar { return 42; }",
"sub baz ($x, $y) { $x + $y }",
"sub qux :method { }",
];
for code in cases {
let mut parser = Parser::new(code);
let result = parser.parse();
assert!(result.is_ok(), "Failed to parse: {}", code);
let ast = must(result);
if let NodeKind::Program { statements } = &ast.kind {
assert_eq!(statements.len(), 1);
assert!(matches!(statements[0].kind, NodeKind::Subroutine { .. }));
}
}
}
#[test]
fn test_control_flow() {
let cases = vec![
"if ($x) { }",
"if ($x) { } else { }",
"if ($x) { } elsif ($y) { } else { }",
"unless ($x) { }",
"while ($x) { }",
"until ($x) { }",
"for (my $i = 0; $i < 10; $i++) { }",
"foreach my $x (@array) { }",
];
for code in cases {
let mut parser = Parser::new(code);
let result = parser.parse();
assert!(result.is_ok(), "Failed to parse: {}", code);
}
}
#[test]
fn test_regex() {
let cases = vec![
"/pattern/",
"m/pattern/",
"s/old/new/",
"tr/a-z/A-Z/",
r#"qr/\d+/"#,
"$x =~ /foo/",
"$x !~ /bar/",
];
for code in cases {
let mut parser = Parser::new(code);
let result = parser.parse();
assert!(result.is_ok(), "Failed to parse: {}", code);
}
}
#[test]
fn test_error_cases() {
let cases = vec![
("if (", "Unexpected end of input"),
("sub (", "Unexpected end of input"),
("my (", "Unexpected end of input"),
("{", "Unexpected end of input"),
];
for (code, _expected_error) in cases {
let mut parser = Parser::new(code);
let result = parser.parse();
assert!(result.is_ok(), "Parser should recover from errors for: {}", code);
let errors = parser.errors();
assert!(!errors.is_empty(), "Expected recorded errors for: {}", code);
}
}
#[test]
fn test_modern_perl_features() {
let cases = vec![
"class Point { }",
"method new { }",
"try { } catch ($e) { }",
"defer { }",
"my $x :shared = 42;",
];
for code in cases {
let mut parser = Parser::new(code);
let result = parser.parse();
assert!(result.is_ok(), "Failed to parse: {}", code);
}
}
#[test]
fn test_edge_cases() {
let cases = vec![
"print STDOUT 'hello';",
"new Class;",
"my ($x, $y) = (1, 2);",
"my ($a :shared, $b :locked);",
"$x->@*",
"$x->%*",
"$x->$*",
"$x // 'default'",
"$obj ISA 'Class'",
];
for code in cases {
let mut parser = Parser::new(code);
let result = parser.parse();
assert!(result.is_ok(), "Failed to parse edge case: {}", code);
}
}
}