Files
modeling-app/rust/kcl-lib/src/lint/checks/camel_case.rs
Adam Chalmers 4356885aa2 Bump cargo to 1.88; 2024 edition for kcl-lib (#7618)
This is a big one because the edition changes a fair number of things.
2025-06-26 22:02:54 +00:00

213 lines
6.0 KiB
Rust

use anyhow::Result;
use convert_case::Casing;
use crate::{
SourceRange,
errors::Suggestion,
lint::rule::{Discovered, Finding, def_finding},
parsing::ast::types::{Node as AstNode, ObjectProperty, Program, VariableDeclarator},
walk::Node,
};
def_finding!(
Z0001,
"Identifiers should be lowerCamelCase",
"\
By convention, variable names are lowerCamelCase, not snake_case, kebab-case,
nor upper CamelCase (aka PascalCase). 🐪
For instance, a good identifier for the variable representing 'box height'
would be 'boxHeight', not 'BOX_HEIGHT', 'box_height' nor 'BoxHeight'. For
more information there's a pretty good Wikipedia page at
https://en.wikipedia.org/wiki/Camel_case
"
);
fn lint_lower_camel_case_var(decl: &VariableDeclarator, prog: &AstNode<Program>) -> Result<Vec<Discovered>> {
let mut findings = vec![];
let ident = &decl.id;
let name = &ident.name;
if !name.is_case(convert_case::Case::Camel) {
// Get what it should be.
let new_name = name.to_case(convert_case::Case::Camel);
let mut prog = prog.clone();
prog.rename_symbol(&new_name, ident.start);
let recast = prog.recast(&Default::default(), 0);
let suggestion = Suggestion {
title: format!("rename '{name}' to '{new_name}'"),
insert: recast,
source_range: prog.as_source_range(),
};
findings.push(Z0001.at(
format!("found '{name}'"),
SourceRange::new(ident.start, ident.end, ident.module_id),
Some(suggestion.clone()),
));
return Ok(findings);
}
Ok(findings)
}
fn lint_lower_camel_case_property(decl: &ObjectProperty, _prog: &AstNode<Program>) -> Result<Vec<Discovered>> {
let mut findings = vec![];
let ident = &decl.key;
let name = &ident.name;
if !name.is_case(convert_case::Case::Camel) {
// We can't rename the properties yet.
findings.push(Z0001.at(
format!("found '{name}'"),
SourceRange::new(ident.start, ident.end, ident.module_id),
None,
));
return Ok(findings);
}
Ok(findings)
}
pub fn lint_variables(decl: Node, prog: &AstNode<Program>) -> Result<Vec<Discovered>> {
let Node::VariableDeclaration(decl) = decl else {
return Ok(vec![]);
};
lint_lower_camel_case_var(&decl.declaration, prog)
}
pub fn lint_object_properties(decl: Node, prog: &AstNode<Program>) -> Result<Vec<Discovered>> {
let Node::ObjectExpression(decl) = decl else {
return Ok(vec![]);
};
Ok(decl
.properties
.iter()
.flat_map(|v| lint_lower_camel_case_property(v, prog).unwrap_or_default())
.collect())
}
#[cfg(test)]
mod tests {
use super::{Z0001, lint_object_properties, lint_variables};
use crate::lint::rule::{assert_finding, test_finding, test_no_finding};
#[tokio::test]
async fn z0001_const() {
assert_finding!(
lint_variables,
Z0001,
"Thickness = 0.5",
"found 'Thickness'",
Some("thickness = 0.5\n".to_string())
);
assert_finding!(
lint_variables,
Z0001,
"THICKNESS = 0.5",
"found 'THICKNESS'",
Some("thickness = 0.5\n".to_string())
);
assert_finding!(
lint_variables,
Z0001,
"THICC_NES = 0.5",
"found 'THICC_NES'",
Some("thiccNes = 0.5\n".to_string())
);
assert_finding!(
lint_variables,
Z0001,
"thicc_nes = 0.5",
"found 'thicc_nes'",
Some("thiccNes = 0.5\n".to_string())
);
assert_finding!(
lint_variables,
Z0001,
"myAPIVar = 0.5",
"found 'myAPIVar'",
Some("myApiVar = 0.5\n".to_string())
);
}
const FULL_BAD: &str = "\
// Define constants
pipeLength = 40
pipeSmallDia = 10
pipeLargeDia = 20
thickness = 0.5
// Create the sketch to be revolved around the y-axis. Use the small diameter, large diameter, length, and thickness to define the sketch.
Part001 = startSketchOn(XY)
|> startProfile(at = [pipeLargeDia - (thickness / 2), 38])
|> line(end = [thickness, 0])
|> line(end = [0, -1])
|> angledLine(angle = 60, endAbsoluteX = pipeSmallDia + thickness)
|> line(end = [0, -pipeLength])
|> angledLine(angle = -60, endAbsoluteX = pipeLargeDia + thickness)
|> line(end = [0, -1])
|> line(end = [-thickness, 0])
|> line(end = [0, 1])
|> angledLine(angle = 120, endAbsoluteX = pipeSmallDia)
|> line(end = [0, pipeLength])
|> angledLine(angle = 60, endAbsoluteX = pipeLargeDia)
|> close()
|> revolve(axis = Y)
";
test_finding!(
z0001_full_bad,
lint_variables,
Z0001,
FULL_BAD,
"found 'Part001'",
Some(FULL_BAD.replace("Part001", "part001").to_string())
);
test_no_finding!(
z0001_full_good,
lint_variables,
Z0001,
"\
// Define constants
pipeLength = 40
pipeSmallDia = 10
pipeLargeDia = 20
thickness = 0.5
// Create the sketch to be revolved around the y-axis. Use the small diameter, large diameter, length, and thickness to define the sketch.
part001 = startSketchOn(XY)
|> startProfile(at = [pipeLargeDia - (thickness / 2), 38])
|> line(end = [thickness, 0])
|> line(end = [0, -1])
|> angledLine(angle = 60, endAbsoluteX = pipeSmallDia + thickness)
|> line(end = [0, -pipeLength])
|> angledLine(angle = -60, endAbsoluteX = pipeLargeDia + thickness)
|> line(end = [0, -1])
|> line(end = [-thickness, 0])
|> line(end = [0, 1])
|> angledLine(angle = 120, endAbsoluteX = pipeSmallDia)
|> line(end = [0, pipeLength])
|> angledLine(angle = 60, endAbsoluteX = pipeLargeDia)
|> close()
|> revolve(axis = Y)
"
);
test_finding!(
z0001_full_bad_object,
lint_object_properties,
Z0001,
"\
circ = {angle_start = 0, angle_end = 360, radius = 5}
",
"found 'angle_start'",
None
);
}