diff --git a/CHANGELOG.md b/CHANGELOG.md index 6583717b..a66aa21d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased](https://github.com/Kampfkarren/selene/compare/0.26.1...HEAD) +### Added +- Added new [`approx_constant`](https://kampfkarren.github.io/selene/lints/approx_constant.html) lint, which will check for number literals that approximate constants. ## [0.26.1](https://github.com/Kampfkarren/selene/releases/tag/0.26.1) - 2023-11-11 ### Fixed diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 51fcca18..22894eb4 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -14,6 +14,7 @@ - [Contributing](./contributing.md) - [Lints](./lints/index.md) - [almost_swapped](./lints/almost_swapped.md) + - [approx_constant](./lints/approx_constant.md) - [constant_table_comparison](./lints/constant_table_comparison.md) - [deprecated](./lints/deprecated.md) - [divide_by_zero](./lints/divide_by_zero.md) diff --git a/docs/src/lints/approx_constant.md b/docs/src/lints/approx_constant.md new file mode 100644 index 00000000..e1ffd23c --- /dev/null +++ b/docs/src/lints/approx_constant.md @@ -0,0 +1,17 @@ +# approx_constant +## What it does +Checks for number literals that approximate constants. + +## Why this is bad +Using constants provided by the Lua standard library is more precise. + +## Example +```lua +local x = 3.14 +``` + +...should be written as... + +```lua +local x = math.pi +``` diff --git a/selene-lib/src/lints.rs b/selene-lib/src/lints.rs index fa42553c..17a3b62f 100644 --- a/selene-lib/src/lints.rs +++ b/selene-lib/src/lints.rs @@ -8,6 +8,7 @@ use full_moon::{ast::Ast, node::Node}; use serde::de::DeserializeOwned; pub mod almost_swapped; +pub mod approx_constant; pub mod bad_string_escape; pub mod compare_nan; pub mod constant_table_comparison; diff --git a/selene-lib/src/lints/approx_constant.rs b/selene-lib/src/lints/approx_constant.rs new file mode 100644 index 00000000..c83ce6bd --- /dev/null +++ b/selene-lib/src/lints/approx_constant.rs @@ -0,0 +1,87 @@ +use super::*; +use std::convert::Infallible; + +use full_moon::{ast::Ast, tokenizer::TokenType, visitors::Visitor}; + +pub struct ApproxConstantLint; + +impl Lint for ApproxConstantLint { + type Config = (); + type Error = Infallible; + + const SEVERITY: Severity = Severity::Warning; + const LINT_TYPE: LintType = LintType::Correctness; + + fn new((): Self::Config) -> Result { + Ok(ApproxConstantLint) + } + + fn pass(&self, ast: &Ast, _: &Context, _: &AstContext) -> Vec { + let mut visitor = ApproxConstantVisitor { + approx_constants: Vec::new(), + }; + + visitor.visit_ast(ast); + + visitor + .approx_constants + .iter() + .map(|constant| { + Diagnostic::new( + "approx_constant", + format!("`{}` is more precise", constant.constant), + Label::new(constant.range), + ) + }) + .collect() + } +} + +struct ApproxConstantVisitor { + approx_constants: Vec, +} + +struct ApproximatedConstant { + range: (usize, usize), + constant: String, +} + +impl Visitor for ApproxConstantVisitor { + fn visit_number(&mut self, token: &full_moon::tokenizer::Token) { + if let TokenType::Number { text } = token.token_type() { + if is_approx_const(std::f64::consts::PI, text, 3) { + self.approx_constants.push(ApproximatedConstant { + range: (token.start_position().bytes(), token.end_position().bytes()), + constant: "math.pi".to_string(), + }); + } + } + } +} + +#[must_use] +fn is_approx_const(constant: f64, value: &str, min_digits: usize) -> bool { + if value.len() <= min_digits { + false + } else if constant.to_string().starts_with(value) { + // The value is a truncated constant + true + } else { + let round_const = format!("{constant:.*}", value.len() - 2); + value == round_const + } +} + +#[cfg(test)] +mod tests { + use super::{super::test_util::test_lint, *}; + + #[test] + fn test_approx_constant() { + test_lint( + ApproxConstantLint::new(()).unwrap(), + "approx_constant", + "approx_constant", + ); + } +} diff --git a/selene-lib/tests/lints/approx_constant/approx_constant.lua b/selene-lib/tests/lints/approx_constant/approx_constant.lua new file mode 100644 index 00000000..ce8d2987 --- /dev/null +++ b/selene-lib/tests/lints/approx_constant/approx_constant.lua @@ -0,0 +1,18 @@ +local good = 3 +local good = 3.1 +local good = 3.13 +local good = 3.15 +local good = 3.1417 +local good = 3.14159266 + +local good = 0x314 +local good = 3_14 + +local bad = 3.14 +local bad = 3.141 +local bad = 3.142 +local bad = 3.1415 +local bad = 3.14159265 + +local bad = 3.14 + 1 +local bad = f(3.14 + 1) diff --git a/selene-lib/tests/lints/approx_constant/approx_constant.stderr b/selene-lib/tests/lints/approx_constant/approx_constant.stderr new file mode 100644 index 00000000..e84a8fcd --- /dev/null +++ b/selene-lib/tests/lints/approx_constant/approx_constant.stderr @@ -0,0 +1,42 @@ +error[approx_constant]: `math.pi` is more precise + ┌─ approx_constant.lua:11:13 + │ +11 │ local bad = 3.14 + │ ^^^^ + +error[approx_constant]: `math.pi` is more precise + ┌─ approx_constant.lua:12:13 + │ +12 │ local bad = 3.141 + │ ^^^^^ + +error[approx_constant]: `math.pi` is more precise + ┌─ approx_constant.lua:13:13 + │ +13 │ local bad = 3.142 + │ ^^^^^ + +error[approx_constant]: `math.pi` is more precise + ┌─ approx_constant.lua:14:13 + │ +14 │ local bad = 3.1415 + │ ^^^^^^ + +error[approx_constant]: `math.pi` is more precise + ┌─ approx_constant.lua:15:13 + │ +15 │ local bad = 3.14159265 + │ ^^^^^^^^^^ + +error[approx_constant]: `math.pi` is more precise + ┌─ approx_constant.lua:17:13 + │ +17 │ local bad = 3.14 + 1 + │ ^^^^ + +error[approx_constant]: `math.pi` is more precise + ┌─ approx_constant.lua:18:15 + │ +18 │ local bad = f(3.14 + 1) + │ ^^^^ +