kcl_lib/lint/checks/
camel_case.rs

1use anyhow::Result;
2use convert_case::Casing;
3
4use crate::{
5    SourceRange,
6    errors::Suggestion,
7    lint::rule::{Discovered, Finding, def_finding},
8    parsing::ast::types::{Node as AstNode, ObjectProperty, Program, VariableDeclarator},
9    walk::Node,
10};
11
12def_finding!(
13    Z0001,
14    "Identifiers should be lowerCamelCase",
15    "\
16By convention, variable names are lowerCamelCase, not snake_case, kebab-case,
17nor upper CamelCase (aka PascalCase). 🐪
18
19For instance, a good identifier for the variable representing 'box height'
20would be 'boxHeight', not 'BOX_HEIGHT', 'box_height' nor 'BoxHeight'. For
21more information there's a pretty good Wikipedia page at
22
23https://en.wikipedia.org/wiki/Camel_case
24",
25    crate::lint::rule::FindingFamily::Style
26);
27
28fn lint_lower_camel_case_var(decl: &VariableDeclarator, prog: &AstNode<Program>) -> Result<Vec<Discovered>> {
29    let mut findings = vec![];
30    let ident = &decl.id;
31    let name = &ident.name;
32
33    if !name.is_case(convert_case::Case::Camel) {
34        // Get what it should be.
35        let new_name = name.to_case(convert_case::Case::Camel);
36
37        let mut prog = prog.clone();
38        prog.rename_symbol(&new_name, ident.start);
39        let recast = prog.recast_top(&Default::default(), 0);
40
41        let suggestion = Suggestion {
42            title: format!("rename '{name}' to '{new_name}'"),
43            insert: recast,
44            source_range: prog.as_source_range(),
45        };
46        findings.push(Z0001.at(
47            format!("found '{name}'"),
48            SourceRange::new(ident.start, ident.end, ident.module_id),
49            Some(suggestion),
50        ));
51        return Ok(findings);
52    }
53
54    Ok(findings)
55}
56
57fn lint_lower_camel_case_property(decl: &ObjectProperty, _prog: &AstNode<Program>) -> Result<Vec<Discovered>> {
58    let mut findings = vec![];
59    let ident = &decl.key;
60    let name = &ident.name;
61
62    if !name.is_case(convert_case::Case::Camel) {
63        // We can't rename the properties yet.
64        findings.push(Z0001.at(
65            format!("found '{name}'"),
66            SourceRange::new(ident.start, ident.end, ident.module_id),
67            None,
68        ));
69        return Ok(findings);
70    }
71
72    Ok(findings)
73}
74
75pub fn lint_variables(decl: Node, prog: &AstNode<Program>) -> Result<Vec<Discovered>> {
76    let Node::VariableDeclaration(decl) = decl else {
77        return Ok(vec![]);
78    };
79
80    lint_lower_camel_case_var(&decl.declaration, prog)
81}
82
83pub fn lint_object_properties(decl: Node, prog: &AstNode<Program>) -> Result<Vec<Discovered>> {
84    let Node::ObjectExpression(decl) = decl else {
85        return Ok(vec![]);
86    };
87
88    Ok(decl
89        .properties
90        .iter()
91        .flat_map(|v| lint_lower_camel_case_property(v, prog).unwrap_or_default())
92        .collect())
93}
94
95#[cfg(test)]
96mod tests {
97    use super::{Z0001, lint_object_properties, lint_variables};
98    use crate::lint::rule::{assert_finding, test_finding, test_no_finding};
99
100    #[tokio::test]
101    async fn z0001_const() {
102        assert_finding!(
103            lint_variables,
104            Z0001,
105            "Thickness = 0.5",
106            "found 'Thickness'",
107            Some("thickness = 0.5\n".to_string())
108        );
109        assert_finding!(
110            lint_variables,
111            Z0001,
112            "THICKNESS = 0.5",
113            "found 'THICKNESS'",
114            Some("thickness = 0.5\n".to_string())
115        );
116        assert_finding!(
117            lint_variables,
118            Z0001,
119            "THICC_NES = 0.5",
120            "found 'THICC_NES'",
121            Some("thiccNes = 0.5\n".to_string())
122        );
123        assert_finding!(
124            lint_variables,
125            Z0001,
126            "thicc_nes = 0.5",
127            "found 'thicc_nes'",
128            Some("thiccNes = 0.5\n".to_string())
129        );
130        assert_finding!(
131            lint_variables,
132            Z0001,
133            "myAPIVar = 0.5",
134            "found 'myAPIVar'",
135            Some("myApiVar = 0.5\n".to_string())
136        );
137    }
138
139    const FULL_BAD: &str = "\
140// Define constants
141pipeLength = 40
142pipeSmallDia = 10
143pipeLargeDia = 20
144thickness = 0.5
145
146// Create the sketch to be revolved around the y-axis. Use the small diameter, large diameter, length, and thickness to define the sketch.
147Part001 = startSketchOn(XY)
148  |> startProfile(at = [pipeLargeDia - (thickness / 2), 38])
149  |> line(end = [thickness, 0])
150  |> line(end = [0, -1])
151  |> angledLine(angle = 60, endAbsoluteX = pipeSmallDia + thickness)
152  |> line(end = [0, -pipeLength])
153  |> angledLine(angle = -60, endAbsoluteX = pipeLargeDia + thickness)
154  |> line(end = [0, -1])
155  |> line(end = [-thickness, 0])
156  |> line(end = [0, 1])
157  |> angledLine(angle = 120, endAbsoluteX = pipeSmallDia)
158  |> line(end = [0, pipeLength])
159  |> angledLine(angle = 60, endAbsoluteX = pipeLargeDia)
160  |> close()
161  |> revolve(axis = Y)
162";
163
164    test_finding!(
165        z0001_full_bad,
166        lint_variables,
167        Z0001,
168        FULL_BAD,
169        "found 'Part001'",
170        Some(FULL_BAD.replace("Part001", "part001"))
171    );
172
173    test_no_finding!(
174        z0001_full_good,
175        lint_variables,
176        Z0001,
177        "\
178// Define constants
179pipeLength = 40
180pipeSmallDia = 10
181pipeLargeDia = 20
182thickness = 0.5
183
184// Create the sketch to be revolved around the y-axis. Use the small diameter, large diameter, length, and thickness to define the sketch.
185part001 = startSketchOn(XY)
186  |> startProfile(at = [pipeLargeDia - (thickness / 2), 38])
187  |> line(end = [thickness, 0])
188  |> line(end = [0, -1])
189  |> angledLine(angle = 60, endAbsoluteX = pipeSmallDia + thickness)
190  |> line(end = [0, -pipeLength])
191  |> angledLine(angle = -60, endAbsoluteX = pipeLargeDia + thickness)
192  |> line(end = [0, -1])
193  |> line(end = [-thickness, 0])
194  |> line(end = [0, 1])
195  |> angledLine(angle = 120, endAbsoluteX = pipeSmallDia)
196  |> line(end = [0, pipeLength])
197  |> angledLine(angle = 60, endAbsoluteX = pipeLargeDia)
198  |> close()
199  |> revolve(axis = Y)
200"
201    );
202
203    test_finding!(
204        z0001_full_bad_object,
205        lint_object_properties,
206        Z0001,
207        "\
208circ = {angle_start = 0, angle_end = 360, radius = 5}
209",
210        "found 'angle_start'",
211        None
212    );
213}