pub mod rust_leakage;
use crate::analyzer::AnalyzedFunction;
use crate::error::SourceLocation;
use crate::parser::{Expression, Statement};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LintLevel {
Error, Warning, Note, Allow, }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LintCategory {
Performance, Correctness, Style, Complexity, }
#[derive(Debug, Clone)]
pub struct LintDiagnostic {
pub lint_name: String,
pub category: LintCategory,
pub level: LintLevel,
pub message: String,
pub location: SourceLocation,
pub help: Option<String>,
pub note: Option<String>,
pub suggestion: Option<String>,
}
impl fmt::Display for LintDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let level_str = match self.level {
LintLevel::Error => "error",
LintLevel::Warning => "warning",
LintLevel::Note => "note",
LintLevel::Allow => return Ok(()), };
writeln!(f, "{}: {} [{}]", level_str, self.message, self.lint_name)?;
writeln!(
f,
" --> {}:{}:{}",
self.location.file, self.location.line, self.location.column
)?;
if let Some(note) = &self.note {
writeln!(f, " = note: {}", note)?;
}
if let Some(help) = &self.help {
writeln!(f, " = help: {}", help)?;
}
if let Some(suggestion) = &self.suggestion {
writeln!(f, " = suggestion: {}", suggestion)?;
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct LintCollector {
diagnostics: Vec<LintDiagnostic>,
}
impl LintCollector {
pub fn new() -> Self {
Self {
diagnostics: Vec::new(),
}
}
pub fn add(&mut self, diagnostic: LintDiagnostic) {
self.diagnostics.push(diagnostic);
}
pub fn diagnostics(&self) -> &[LintDiagnostic] {
&self.diagnostics
}
pub fn into_diagnostics(self) -> Vec<LintDiagnostic> {
self.diagnostics
}
pub fn has_errors(&self) -> bool {
self.diagnostics.iter().any(|d| d.level == LintLevel::Error)
}
pub fn has_warnings(&self) -> bool {
self.diagnostics
.iter()
.any(|d| d.level == LintLevel::Warning)
}
}
pub struct Linter<'ast> {
collector: LintCollector,
_phantom: std::marker::PhantomData<&'ast ()>,
}
impl<'ast> Default for Linter<'ast> {
fn default() -> Self {
Self::new()
}
}
impl<'ast> Linter<'ast> {
pub fn new() -> Self {
Self {
collector: LintCollector::new(),
_phantom: std::marker::PhantomData,
}
}
pub fn lint_function(&mut self, analyzed: &AnalyzedFunction<'ast>) {
self.lint_owned_but_not_returned(analyzed);
}
fn lint_owned_but_not_returned(&mut self, analyzed: &AnalyzedFunction<'ast>) {
use crate::analyzer::OwnershipMode;
for param in &analyzed.decl.parameters {
let ownership = analyzed
.inferred_ownership
.get(¶m.name)
.unwrap_or(&OwnershipMode::Owned);
if !matches!(ownership, OwnershipMode::Owned) {
continue; }
let is_mutated = analyzed.mutated_parameters.contains(¶m.name);
if !is_mutated {
continue; }
let is_returned = Self::parameter_is_returned(¶m.name, &analyzed.decl.body);
if is_returned {
continue; }
use crate::parser::Type;
if matches!(
param.type_,
Type::Int | Type::Int32 | Type::Uint | Type::Float | Type::Bool
) {
continue;
}
let location = SourceLocation {
file: format!("{}.wj", analyzed.decl.name), line: 1,
column: 1,
};
self.collector.add(LintDiagnostic {
lint_name: "owned-but-not-returned".to_string(),
category: LintCategory::Performance,
level: LintLevel::Warning,
message: format!("Parameter `{}` is mutated but not returned", param.name),
location,
help: Some(format!(
"Consider using `&mut {}` for efficiency",
match ¶m.type_ {
crate::parser::Type::Custom(name) => name.clone(),
_ => format!("{:?}", param.type_),
}
)),
note: Some(
"Owned parameters that aren't returned waste a move operation".to_string(),
),
suggestion: Some(format!(
"Change `{}: {}` to `{}: &mut {}`",
param.name,
match ¶m.type_ {
crate::parser::Type::Custom(name) => name.clone(),
crate::parser::Type::String => "string".to_string(),
_ => format!("{:?}", param.type_),
},
param.name,
match ¶m.type_ {
crate::parser::Type::Custom(name) => name.clone(),
crate::parser::Type::String => "string".to_string(),
_ => format!("{:?}", param.type_),
}
)),
});
}
}
fn parameter_is_returned(param_name: &str, body: &[&Statement]) -> bool {
for stmt in body {
match stmt {
Statement::Return {
value: Some(expr), ..
}
if Self::expression_contains_identifier(expr, param_name) => {
return true;
}
Statement::Expression { expr, .. }
if Self::expression_contains_identifier(expr, param_name) => {
return true;
}
_ => {}
}
}
false
}
fn expression_contains_identifier(expr: &Expression, name: &str) -> bool {
match expr {
Expression::Identifier { name: id_name, .. } => id_name == name,
Expression::FieldAccess { object, .. } => {
Self::expression_contains_identifier(object, name)
}
_ => false,
}
}
pub fn into_diagnostics(self) -> Vec<LintDiagnostic> {
self.collector.diagnostics
}
pub fn diagnostics_by_level(&self, level: LintLevel) -> Vec<&LintDiagnostic> {
self.collector
.diagnostics
.iter()
.filter(|d| d.level == level)
.collect()
}
}
#[cfg(test)]
mod owned_but_not_returned_tests {
use super::Linter;
use crate::analyzer::{AnalyzedFunction, OwnershipMode};
use crate::auto_clone::AutoCloneAnalysis;
use crate::parser::ast::core::{FunctionDecl, Parameter};
use crate::parser::ast::ownership::OwnershipHint;
use crate::parser::ast::types::Type;
use std::collections::{HashMap, HashSet};
#[test]
fn lint_fires_for_owned_mutated_not_returned() {
let decl = FunctionDecl {
name: "fill_pool".to_string(),
is_pub: true,
is_extern: false,
type_params: vec![],
where_clause: vec![],
decorators: vec![],
is_async: false,
parameters: vec![Parameter {
name: "pool".to_string(),
pattern: None,
type_: Type::Custom("ResourcePool".to_string()),
ownership: OwnershipHint::Inferred,
is_mutable: false,
decorators: vec![],
}],
return_type: None,
return_decorators: vec![],
body: vec![],
parent_type: None,
impl_trait: None,
doc_comment: None,
};
let analyzed = AnalyzedFunction {
decl,
inferred_ownership: {
let mut m = HashMap::new();
m.insert("pool".to_string(), OwnershipMode::Owned);
m
},
inferred_param_types: vec![Type::Custom("ResourcePool".to_string())],
mutated_variables: HashSet::new(),
mutated_parameters: {
let mut s = HashSet::new();
s.insert("pool".to_string());
s
},
auto_clone_analysis: AutoCloneAnalysis::default(),
clone_optimizations: vec![],
struct_mapping_optimizations: vec![],
string_optimizations: vec![],
assignment_optimizations: vec![],
defer_drop_optimizations: vec![],
const_static_optimizations: vec![],
smallvec_optimizations: vec![],
cow_optimizations: vec![],
cache_locality: crate::analyzer::CacheLocalityAnalysis::default(),
str_ref_optimizable_params: HashSet::new(),
};
let mut linter = Linter::new();
linter.lint_function(&analyzed);
let diags = linter.into_diagnostics();
assert!(
diags
.iter()
.any(|d| d.lint_name == "owned-but-not-returned"),
"expected owned-but-not-returned, got: {:?}",
diags
);
}
}