├── website ├── .prettierrc.json ├── public │ ├── favicon.ico │ ├── apple-touch-icon.png │ └── favicon.svg ├── index.md ├── .vitepress │ ├── lints_sidebar.mjs │ ├── syntaxes.mjs │ ├── theme │ │ ├── index.js │ │ ├── Layout.vue │ │ └── style.css │ ├── components │ │ ├── home │ │ │ └── Download.vue │ │ ├── Home.vue │ │ └── Footer.vue │ └── config.mjs ├── docs │ ├── default_profile.md │ └── lints │ │ ├── panic.md │ │ ├── clone_on_copy.md │ │ ├── bitwise_for_parity_check.md │ │ ├── double_parens.md │ │ ├── div_eq_op.md │ │ ├── eq_comp_op.md │ │ ├── eq_diff_op.md │ │ ├── equatable_if_let.md │ │ ├── duplicate_underscore_args.md │ │ ├── eq_bitwise_op.md │ │ ├── eq_logical_op.md │ │ ├── impossible_comparison.md │ │ ├── redundant_op.md │ │ ├── unit_return_type.md │ │ ├── break_unit.md │ │ ├── manual_unwrap_or.md │ │ ├── int_ge_min_one.md │ │ ├── int_ge_plus_one.md │ │ ├── int_le_min_one.md │ │ ├── int_le_plus_one.md │ │ ├── empty_enum_brackets_variant.md │ │ ├── equality_match.md │ │ ├── destruct_match.md │ │ ├── enum_variant_names.md │ │ ├── manual_assert.md │ │ ├── neq_comp_op.md │ │ ├── bool_comparison.md │ │ ├── inefficient_while_comp.md │ │ ├── manual_is_none.md │ │ ├── manual_is_some.md │ │ ├── manual_is_ok.md │ │ ├── manual_is_err.md │ │ ├── manual_unwrap_or_default.md │ │ ├── erasing_op.md │ │ ├── manual_expect.md │ │ ├── manual_err.md │ │ ├── manual_ok_or.md │ │ ├── redundant_brackets_in_enum_call.md │ │ ├── loop_match_pop_front.md │ │ ├── manual_ok.md │ │ ├── contradictory_comparison.md │ │ ├── redundant_comparison.md │ │ ├── ifs_same_cond.md │ │ ├── loop_for_while.md │ │ ├── collapsible_if.md │ │ ├── manual_expect_err.md │ │ ├── simplifiable_comparison.md │ │ ├── collapsible_if_else.md │ │ └── unwrap_syscall.md ├── package.json ├── .gitignore └── docs.md ├── .tool-versions ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── workflows │ ├── on-scarb-new-tag.yml │ ├── nightly.yml │ ├── website-deploy.yml │ └── ci.yml └── dependabot.yml ├── tests ├── performance │ └── mod.rs ├── loops │ └── mod.rs ├── ifs │ └── mod.rs ├── manual │ └── mod.rs ├── main.rs ├── fix_messages │ └── mod.rs ├── helpers │ ├── setup.rs │ └── scarb.rs ├── redundant_op │ └── mod.rs ├── breaks │ └── mod.rs ├── empty_enum_brackets_variant │ └── mod.rs ├── duplicate_underscore_args │ └── mod.rs ├── unused_variables │ └── mod.rs ├── collapsible_match │ └── mod.rs ├── bitwise_for_parity_check │ └── mod.rs ├── erasing_operations │ └── mod.rs ├── eq_op │ └── mod.rs └── unit_return_type │ └── mod.rs ├── src ├── lints │ ├── performance │ │ ├── mod.rs │ │ └── inefficient_while_comp.rs │ ├── loops │ │ └── mod.rs │ ├── ifs │ │ ├── mod.rs │ │ ├── ifs_same_cond.rs │ │ └── equatable_if_let.rs │ ├── duplicate_underscore_args.rs │ ├── redundant_op.rs │ ├── erasing_op.rs │ ├── mod.rs │ ├── bitwise_for_parity_check.rs │ ├── breaks.rs │ ├── empty_enum_brackets_variant.rs │ ├── manual │ │ ├── manual_err.rs │ │ ├── manual_ok.rs │ │ ├── manual_ok_or.rs │ │ ├── manual_expect_err.rs │ │ └── manual_expect.rs │ ├── unit_return_type.rs │ ├── redundant_into.rs │ └── panic.rs ├── diagnostics.rs ├── plugin.rs ├── mappings.rs └── fixer │ └── db.rs ├── .gitignore ├── xtask ├── src │ ├── upgrade.rs │ ├── sync_version.rs │ └── main.rs └── Cargo.toml ├── .cargo └── config.toml ├── .editorconfig ├── lint_docs_template.html ├── README.md ├── SECURITY.md ├── MAINTAINING.md ├── ROADMAP.md ├── Cargo.toml └── CONTRIBUTING.md /website/.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | scarb nightly-2025-11-01 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @software-mansion/cairo-lint 2 | -------------------------------------------------------------------------------- /tests/performance/mod.rs: -------------------------------------------------------------------------------- 1 | mod inefficient_while_comp; 2 | -------------------------------------------------------------------------------- /src/lints/performance/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod inefficient_while_comp; 2 | -------------------------------------------------------------------------------- /tests/loops/mod.rs: -------------------------------------------------------------------------------- 1 | mod loop_for_while; 2 | mod loops_match_pop_front; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea/ 3 | .DS_Store 4 | *.rs.pending-snap 5 | .spr.yml 6 | -------------------------------------------------------------------------------- /xtask/src/upgrade.rs: -------------------------------------------------------------------------------- 1 | pub use cairo_toolchain_xtasks::upgrade::{main, Args}; 2 | -------------------------------------------------------------------------------- /src/lints/loops/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod loop_for_while; 2 | pub mod loop_match_pop_front; 3 | -------------------------------------------------------------------------------- /xtask/src/sync_version.rs: -------------------------------------------------------------------------------- 1 | pub use cairo_toolchain_xtasks::sync_version::{main, Args}; 2 | -------------------------------------------------------------------------------- /tests/ifs/mod.rs: -------------------------------------------------------------------------------- 1 | mod collapsible_if; 2 | mod collapsible_if_else; 3 | mod equatable_if_let; 4 | mod ifs_same_cond; 5 | -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion/cairo-lint/HEAD/website/public/favicon.ico -------------------------------------------------------------------------------- /website/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion/cairo-lint/HEAD/website/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src/lints/ifs/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod collapsible_if; 2 | pub mod collapsible_if_else; 3 | pub mod equatable_if_let; 4 | pub mod ifs_same_cond; 5 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | lint = "clippy --workspace --all-targets --all-features -- --no-deps" 3 | docs = "doc --workspace --all-features --no-deps" 4 | xtask = "run --quiet --package xtask --" 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Cairo Linter Telegram Channel 3 | url: https://t.me/cairolint 4 | about: Have a question or issue? Chat with the community and developers here. 5 | -------------------------------------------------------------------------------- /website/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Cairo lint 4 | titleTemplate: false 5 | --- 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 4 5 | indent_style = space 6 | insert_final_newline = true 7 | max_line_length = 100 8 | trim_trailing_whitespace = true 9 | 10 | [{*.yaml,*.yml}] 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /website/.vitepress/lints_sidebar.mjs: -------------------------------------------------------------------------------- 1 | import LintMetadata from "../lints_metadata.json"; 2 | 3 | export const getLintsSidebar = () => 4 | LintMetadata.map((lint) => ({ 5 | text: lint.name, 6 | link: `/docs/lints/${lint.name}`, 7 | })); 8 | -------------------------------------------------------------------------------- /website/docs/default_profile.md: -------------------------------------------------------------------------------- 1 | # Default Profile 2 | 3 | By default, all lint rules are **enabled** with the exception of: 4 | 5 | - [enum_variant_names](lints/enum_variant_names.md) 6 | - [panic](lints/panic.md) 7 | - [inefficient_while_comp](lints/inefficient_while_comp.md) 8 | -------------------------------------------------------------------------------- /website/.vitepress/syntaxes.mjs: -------------------------------------------------------------------------------- 1 | import syntax from "cairo-tm-grammar"; 2 | 3 | export const cairo = { 4 | ...syntax, 5 | 6 | // NODE: The Cairo syntax uses capital-case for language name, 7 | // which is interpreted differently by Shiki, hence this override. 8 | name: "cairo", 9 | }; 10 | -------------------------------------------------------------------------------- /website/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | // https://vitepress.dev/guide/custom-theme 2 | import Theme from "vitepress/theme"; 3 | import "./style.css"; 4 | import Layout from "./Layout.vue"; 5 | 6 | export default { 7 | extends: Theme, 8 | Layout, 9 | enhanceApp({ app, router, siteData }) {}, 10 | }; 11 | -------------------------------------------------------------------------------- /tests/manual/mod.rs: -------------------------------------------------------------------------------- 1 | mod manual_assert; 2 | mod manual_err; 3 | mod manual_expect; 4 | mod manual_expect_err; 5 | mod manual_is_empty; 6 | mod manual_is_err; 7 | mod manual_is_none; 8 | mod manual_is_ok; 9 | mod manual_is_some; 10 | mod manual_ok; 11 | mod manual_ok_or; 12 | mod manual_unwrap_or; 13 | mod manual_unwrap_or_default; 14 | -------------------------------------------------------------------------------- /website/docs/lints/panic.md: -------------------------------------------------------------------------------- 1 | # panic 2 | 3 | Default: **Disabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/panic.rs#L29) 6 | 7 | ## What it does 8 | 9 | Checks for panic usages. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | panic!("panic"); 16 | } 17 | ``` 18 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "1.0.0" 4 | edition.workspace = true 5 | publish = false 6 | 7 | [dependencies] 8 | anyhow = "1.0.100" 9 | cairo-toolchain-xtasks = "1" 10 | cairo-lint = { path = "../" } 11 | clap = { version = "4.5.53", features = ["derive"]} 12 | serde = { version = "1", features = ["serde_derive"] } 13 | serde_json = { version = "1" } 14 | -------------------------------------------------------------------------------- /website/docs/lints/clone_on_copy.md: -------------------------------------------------------------------------------- 1 | # clone_on_copy 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/clone_on_copy.rs#L34) 6 | 7 | ## What it does 8 | 9 | Checks for usage of `.clone()` on a `Copy` type. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | let a: felt252 = 'Hello'; 15 | let b = a.clone() 16 | ``` 17 | -------------------------------------------------------------------------------- /src/diagnostics.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_diagnostics::DiagnosticEntry; 2 | use cairo_lang_diagnostics::format_diagnostics as cairo_format_diagnostics; 3 | use cairo_lang_semantic::SemanticDiagnostic; 4 | use salsa::Database; 5 | 6 | pub fn format_diagnostic(diagnostic: &SemanticDiagnostic, db: &dyn Database) -> String { 7 | cairo_format_diagnostics(db, &diagnostic.format(db), diagnostic.location(db)) 8 | } 9 | -------------------------------------------------------------------------------- /lint_docs_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Cairo lints 10 | 11 | 12 | 13 | {{ name }} 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/on-scarb-new-tag.yml: -------------------------------------------------------------------------------- 1 | name: Respond to New Scarb Tag 2 | 3 | on: 4 | repository_dispatch: 5 | types: [scarb-new-tag-published] 6 | 7 | permissions: 8 | contents: read 9 | pages: write 10 | id-token: write 11 | 12 | jobs: 13 | run-on-tag: 14 | uses: software-mansion/cairo-lint/.github/workflows/website-deploy.yml@main 15 | with: 16 | target_branch: "${{ github.event.client_payload.tag }}" 17 | -------------------------------------------------------------------------------- /website/docs/lints/bitwise_for_parity_check.md: -------------------------------------------------------------------------------- 1 | # bitwise_for_parity_check 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/bitwise_for_parity_check.rs#L28) 6 | 7 | ## What it does 8 | 9 | Checks for `x & 1` which is unoptimized in cairo and could be replaced by `x % 1`. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let _a = 200_u32 & 1; 16 | } 17 | ``` 18 | -------------------------------------------------------------------------------- /website/docs/lints/double_parens.md: -------------------------------------------------------------------------------- 1 | # double_parens 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/double_parens.rs#L34) 6 | 7 | ## What it does 8 | 9 | Checks for unnecessary double parentheses in expressions. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() -> u32 { 15 | ((0)) 16 | } 17 | ``` 18 | 19 | Can be simplified to: 20 | 21 | ```cairo 22 | fn main() -> u32 { 23 | 0 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /website/docs/lints/div_eq_op.md: -------------------------------------------------------------------------------- 1 | # div_eq_op 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/eq_op.rs#L36) 6 | 7 | ## What it does 8 | 9 | Checks for division with identical operands. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn foo(a: u256) -> u256 { 15 | a / a 16 | } 17 | ``` 18 | 19 | Could be simplified by replacing the entire expression with 1: 20 | 21 | ```cairo 22 | fn foo(a: u256) -> u256 { 23 | 1 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /website/docs/lints/eq_comp_op.md: -------------------------------------------------------------------------------- 1 | # eq_comp_op 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/eq_op.rs#L72) 6 | 7 | ## What it does 8 | 9 | Checks for comparison with identical operands. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn foo(a: u256) -> bool { 15 | a == a 16 | } 17 | ``` 18 | 19 | Could be simplified by replacing the entire expression with true: 20 | 21 | ```cairo 22 | fn foo(a: u256) -> bool { 23 | true 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /website/docs/lints/eq_diff_op.md: -------------------------------------------------------------------------------- 1 | # eq_diff_op 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/eq_op.rs#L146) 6 | 7 | ## What it does 8 | 9 | Checks for subtraction with identical operands. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn foo(a: u256) -> u256 { 15 | a - a 16 | } 17 | ``` 18 | 19 | Could be simplified by replacing the entire expression with zero: 20 | 21 | ```cairo 22 | fn foo(a: u256) -> u256 { 23 | 0 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /website/docs/lints/equatable_if_let.md: -------------------------------------------------------------------------------- 1 | # equatable_if_let 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/ifs/equatable_if_let.rs#L37) 6 | 7 | ## What it does 8 | 9 | Checks for `if let` pattern matching that can be replaced by a simple comparison. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | if let Some(2) = a { 15 | // Code 16 | } 17 | ``` 18 | 19 | Could be replaced by 20 | 21 | ```cairo 22 | if a == Some(2) { 23 | // Code 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /website/docs/lints/duplicate_underscore_args.md: -------------------------------------------------------------------------------- 1 | # duplicate_underscore_args 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/duplicate_underscore_args.rs#L23) 6 | 7 | ## What it does 8 | 9 | Checks for functions that have the same argument name but prefix with `_`. 10 | 11 | ## Example 12 | 13 | This code will raise a warning because it can be difficult to differentiate between `test` and `_test`. 14 | 15 | ```cairo 16 | fn foo(test: u32, _test: u32) {} 17 | ``` 18 | -------------------------------------------------------------------------------- /website/docs/lints/eq_bitwise_op.md: -------------------------------------------------------------------------------- 1 | # eq_bitwise_op 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/eq_op.rs#L181) 6 | 7 | ## What it does 8 | 9 | Checks for bitwise operation with identical operands. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn foo(a: u256) -> u256 { 15 | a & a 16 | } 17 | ``` 18 | 19 | Could be simplified by replacing the entire expression with the operand: 20 | 21 | ```cairo 22 | fn foo(a: u256) -> u256 { 23 | a 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /website/docs/lints/eq_logical_op.md: -------------------------------------------------------------------------------- 1 | # eq_logical_op 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/eq_op.rs#L217) 6 | 7 | ## What it does 8 | 9 | Checks for logical operation with identical operands. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn foo(a: u256) -> u256 { 15 | a & a 16 | } 17 | ``` 18 | 19 | Could be simplified by replacing the entire expression with the operand: 20 | 21 | ```cairo 22 | fn foo(a: u256) -> u256 { 23 | a 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /website/docs/lints/impossible_comparison.md: -------------------------------------------------------------------------------- 1 | # impossible_comparison 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/double_comparison.rs#L38) 6 | 7 | ## What it does 8 | 9 | Checks for impossible comparisons. Those ones always return false. 10 | 11 | ## Example 12 | 13 | Here is an example of impossible comparison: 14 | 15 | ```cairo 16 | fn main() { 17 | let x: u32 = 1; 18 | if x > 200 && x < 100 { 19 | //impossible to reach 20 | } 21 | } 22 | ``` 23 | -------------------------------------------------------------------------------- /website/docs/lints/redundant_op.md: -------------------------------------------------------------------------------- 1 | # redundant_op 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/redundant_op.rs#L36) 6 | 7 | ## What it does 8 | 9 | Checks for redundant arithmetic operations like `x + 0`, `x - 0`, `x * 1`, `x / 1` 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let x = 42; 16 | let _y = x * 1; 17 | } 18 | ``` 19 | 20 | Can be simplified to 21 | 22 | ```cairo 23 | fn main() { 24 | let x = 42; 25 | let _y = x; 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /website/docs/lints/unit_return_type.md: -------------------------------------------------------------------------------- 1 | # unit_return_type 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/unit_return_type.rs#L38) 6 | 7 | ## What it does 8 | 9 | Detects if the function has a unit return type, which is not needed to be specified. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn foo() -> () { 15 | println!("Hello, world!"); 16 | } 17 | ``` 18 | 19 | Can be simplified to just: 20 | 21 | ```cairo 22 | fn foo() { 23 | println!("Hello, world!"); 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /website/docs/lints/break_unit.md: -------------------------------------------------------------------------------- 1 | # break_unit 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/breaks.rs#L39) 6 | 7 | ## What it does 8 | 9 | Checks for `break ();` statements and suggests removing the parentheses. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | loop { 16 | break (); 17 | } 18 | } 19 | ``` 20 | 21 | Can be fixed by removing the parentheses: 22 | 23 | ```cairo 24 | fn main() { 25 | loop { 26 | break; 27 | } 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /website/docs/lints/manual_unwrap_or.md: -------------------------------------------------------------------------------- 1 | # manual_unwrap_or 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/manual/manual_unwrap_or.rs#L39) 6 | 7 | ## What it does 8 | 9 | Finds patterns that reimplement `Option::unwrap_or` or `Result::unwrap_or`. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | let foo: Option = None; 15 | match foo { 16 | Some(v) => v, 17 | None => 1, 18 | }; 19 | ``` 20 | 21 | Can be simplified to: 22 | 23 | ```cairo 24 | let foo: Option = None; 25 | foo.unwrap_or(1); 26 | ``` 27 | -------------------------------------------------------------------------------- /website/.vitepress/theme/Layout.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 25 | -------------------------------------------------------------------------------- /website/docs/lints/int_ge_min_one.md: -------------------------------------------------------------------------------- 1 | # int_ge_min_one 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/int_op_one.rs#L87) 6 | 7 | ## What it does 8 | 9 | Check for unnecessary sub operation in integer >= comparison. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let x: u32 = 1; 16 | let y: u32 = 1; 17 | if x - 1 >= y {} 18 | } 19 | ``` 20 | 21 | Can be simplified to: 22 | 23 | ```cairo 24 | fn main() { 25 | let x: u32 = 1; 26 | let y: u32 = 1; 27 | if x > y {} 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /website/docs/lints/int_ge_plus_one.md: -------------------------------------------------------------------------------- 1 | # int_ge_plus_one 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/int_op_one.rs#L40) 6 | 7 | ## What it does 8 | 9 | Check for unnecessary add operation in integer >= comparison. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let x: u32 = 1; 16 | let y: u32 = 1; 17 | if x >= y + 1 {} 18 | } 19 | ``` 20 | 21 | Can be simplified to: 22 | 23 | ```cairo 24 | fn main() { 25 | let x: u32 = 1; 26 | let y: u32 = 1; 27 | if x > y {} 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /website/docs/lints/int_le_min_one.md: -------------------------------------------------------------------------------- 1 | # int_le_min_one 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/int_op_one.rs#L181) 6 | 7 | ## What it does 8 | 9 | Check for unnecessary sub operation in integer <= comparison. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let x: u32 = 1; 16 | let y: u32 = 1; 17 | if x <= y - 1 {} 18 | } 19 | ``` 20 | 21 | Can be simplified to: 22 | 23 | ```cairo 24 | fn main() { 25 | let x: u32 = 1; 26 | let y: u32 = 1; 27 | if x < y {} 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /website/docs/lints/int_le_plus_one.md: -------------------------------------------------------------------------------- 1 | # int_le_plus_one 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/int_op_one.rs#L134) 6 | 7 | ## What it does 8 | 9 | Check for unnecessary add operation in integer <= comparison. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let x: u32 = 1; 16 | let y: u32 = 1; 17 | if x + 1 <= y {} 18 | } 19 | ``` 20 | 21 | Can be simplified to: 22 | 23 | ```cairo 24 | fn main() { 25 | let x: u32 = 1; 26 | let y: u32 = 1; 27 | if x < y {} 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /website/docs/lints/empty_enum_brackets_variant.md: -------------------------------------------------------------------------------- 1 | # empty_enum_brackets_variant 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/empty_enum_brackets_variant.rs#L38) 6 | 7 | ## What it does 8 | 9 | Finds enum variants that are declared with empty brackets. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | enum MyEnum { 15 | Data: u8, 16 | Empty: () // redundant parentheses 17 | } 18 | ``` 19 | 20 | Can be simplified to: 21 | 22 | ```cairo 23 | enum MyEnum { 24 | Data(u8), 25 | Empty, 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /website/docs/lints/equality_match.md: -------------------------------------------------------------------------------- 1 | # equality_match 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/single_match.rs#L86) 6 | 7 | ## What it does 8 | 9 | Checks for matches that do something only in 1 arm and can be rewrote as an `if` 10 | 11 | ## Example 12 | 13 | ```cairo 14 | match variable { 15 | Option::None => println!("None"), 16 | Option::Some => (), 17 | }; 18 | ``` 19 | 20 | Which can be probably rewritten as 21 | 22 | ```cairo 23 | if variable.is_none() { 24 | println!("None"); 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /website/docs/lints/destruct_match.md: -------------------------------------------------------------------------------- 1 | # destruct_match 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/single_match.rs#L42) 6 | 7 | ## What it does 8 | 9 | Checks for matches that do something only in 1 arm and can be rewrote as an `if let` 10 | 11 | ## Example 12 | 13 | ```cairo 14 | let var = Option::Some(1_u32); 15 | match var { 16 | Option::Some(val) => do_smth(val), 17 | _ => (), 18 | } 19 | ``` 20 | 21 | Which can be rewritten as 22 | 23 | ```cairo 24 | if let Option::Some(val) = var { 25 | do_smth(val), 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /website/docs/lints/enum_variant_names.md: -------------------------------------------------------------------------------- 1 | # enum_variant_names 2 | 3 | Default: **Disabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/enum_variant_names.rs#L37) 6 | 7 | ## What it does 8 | 9 | Detects enumeration variants that are prefixed or suffixed by the same characters. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | enum Cake { 15 | BlackForestCake, 16 | HummingbirdCake, 17 | BattenbergCake, 18 | } 19 | ``` 20 | 21 | Can be simplified to: 22 | 23 | ```cairo 24 | enum Cake { 25 | BlackForest, 26 | Hummingbird, 27 | Battenberg, 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /website/docs/lints/manual_assert.md: -------------------------------------------------------------------------------- 1 | # manual_assert 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/manual/manual_assert.rs#L48) 6 | 7 | ## What it does 8 | 9 | Checks for manual implementations of `assert` macro in `if` expressions. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let a = 5; 16 | if a == 5 { 17 | panic!("a shouldn't be equal to 5"); 18 | } 19 | } 20 | ``` 21 | 22 | Can be rewritten as: 23 | 24 | ```cairo 25 | fn main() { 26 | let a = 5; 27 | assert!(a != 5, "a shouldn't be equal to 5"); 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /website/docs/lints/neq_comp_op.md: -------------------------------------------------------------------------------- 1 | # neq_comp_op 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/eq_op.rs#L111) 6 | 7 | ## What it does 8 | 9 | Checks for arithmetical comparison with identical operands. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn foo(a: u256) -> bool { 15 | let _z = a != a; 16 | let _y = a > a; 17 | a < a 18 | } 19 | ``` 20 | 21 | Could be simplified by replacing the entire expression with false: 22 | 23 | ```cairo 24 | fn foo(a: u256) -> bool { 25 | let _z = false; 26 | let _y = false; 27 | false 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /website/docs/lints/bool_comparison.md: -------------------------------------------------------------------------------- 1 | # bool_comparison 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/bool_comparison.rs#L43) 6 | 7 | ## What it does 8 | 9 | Checks for direct variable with boolean literal like `a == true` or `a == false`. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let x = true; 16 | if x == true { 17 | println!("x is true"); 18 | } 19 | } 20 | ``` 21 | 22 | Can be rewritten as: 23 | 24 | ```cairo 25 | fn main() { 26 | let x = true; 27 | if x { 28 | println!("x is true"); 29 | } 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "cargo" 7 | directory: "/" 8 | schedule: 9 | interval: "monthly" 10 | groups: 11 | cairo: 12 | patterns: 13 | - "cairo-lang-*" 14 | non critical: 15 | patterns: 16 | - "*" 17 | exclude-patterns: 18 | - "cairo-lang-*" 19 | 20 | - package-ecosystem: "github-actions" 21 | directory: "/" 22 | schedule: 23 | interval: "monthly" 24 | -------------------------------------------------------------------------------- /website/docs/lints/inefficient_while_comp.md: -------------------------------------------------------------------------------- 1 | # inefficient_while_comp 2 | 3 | Default: **Disabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/performance/inefficient_while_comp.rs#L37) 6 | 7 | ## What it does 8 | 9 | Checks if the while loop exit condition is using [`<`, `<=`, `>=`, `>`] operators. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let mut a = 1_u32; 16 | while a <= 10 { 17 | a += 1; 18 | } 19 | } 20 | ``` 21 | 22 | Can be optimized to: 23 | 24 | ```cairo 25 | fn main() { 26 | let mut a = 1_u32; 27 | while a != 10 { 28 | a += 1; 29 | } 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /website/docs/lints/manual_is_none.md: -------------------------------------------------------------------------------- 1 | # manual_is_none 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/manual/manual_is.rs#L89) 6 | 7 | ## What it does 8 | 9 | Checks for manual implementations of `is_none`. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let foo: Option = Option::None; 16 | let _foo = match foo { 17 | Option::Some(_) => false, 18 | Option::None => true, 19 | }; 20 | } 21 | ``` 22 | 23 | Can be rewritten as: 24 | 25 | ```cairo 26 | fn main() { 27 | let foo: Option = Option::None; 28 | let _foo = foo.is_none(); 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /website/docs/lints/manual_is_some.md: -------------------------------------------------------------------------------- 1 | # manual_is_some 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/manual/manual_is.rs#L41) 6 | 7 | ## What it does 8 | 9 | Checks for manual implementations of `is_some`. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let foo: Option = Option::None; 16 | let _foo = match foo { 17 | Option::Some(_) => true, 18 | Option::None => false, 19 | }; 20 | } 21 | ``` 22 | 23 | Can be rewritten as: 24 | 25 | ```cairo 26 | fn main() { 27 | let foo: Option = Option::None; 28 | let _foo = foo.is_some(); 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /website/docs/lints/manual_is_ok.md: -------------------------------------------------------------------------------- 1 | # manual_is_ok 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/manual/manual_is.rs#L137) 6 | 7 | ## What it does 8 | 9 | Checks for manual implementations of `is_ok`. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let res_val: Result = Result::Err('err'); 16 | let _a = match res_val { 17 | Result::Ok(_) => true, 18 | Result::Err(_) => false 19 | }; 20 | } 21 | ``` 22 | 23 | Can be rewritten as: 24 | 25 | ```cairo 26 | fn main() { 27 | let res_val: Result = Result::Err('err'); 28 | let _a = res_val.is_ok(); 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /website/docs/lints/manual_is_err.md: -------------------------------------------------------------------------------- 1 | # manual_is_err 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/manual/manual_is.rs#L185) 6 | 7 | ## What it does 8 | 9 | Checks for manual implementations of `is_err`. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let res_val: Result = Result::Err('err'); 16 | let _a = match res_val { 17 | Result::Ok(_) => false, 18 | Result::Err(_) => true 19 | }; 20 | } 21 | ``` 22 | 23 | Can be rewritten as: 24 | 25 | ```cairo 26 | fn main() { 27 | let res_val: Result = Result::Err('err'); 28 | let _a = res_val.is_err(); 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /website/docs/lints/manual_unwrap_or_default.md: -------------------------------------------------------------------------------- 1 | # manual_unwrap_or_default 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/manual/manual_unwrap_or_default.rs#L48) 6 | 7 | ## What it does 8 | 9 | Checks for manual unwrapping of an Option or Result. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let x: Option = Option::Some(1038); 16 | if let Option::Some(v) = x { 17 | v 18 | } else { 19 | 0 20 | }; 21 | } 22 | ``` 23 | 24 | Can be simplified to: 25 | 26 | ```cairo 27 | fn main() { 28 | let x: Option = Option::Some(1038); 29 | x.unwrap_or_default(); 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /website/docs/lints/erasing_op.md: -------------------------------------------------------------------------------- 1 | # erasing_op 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/erasing_op.rs#L41) 6 | 7 | ## What it does 8 | 9 | Checks for operations that result in the value being erased (e.g., multiplication by 0 or 0 being divided by anything). 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let x = 1; 16 | let _y = 0 * x; 17 | let _z = 0 / x; 18 | let _c = x & 0; 19 | } 20 | ``` 21 | 22 | Could be simplified by replacing the entire expression with 0: 23 | 24 | ```cairo 25 | fn main() { 26 | let x = 1; 27 | let _y = 0; 28 | let _z = 0; 29 | let _c = 0; 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /website/docs/lints/manual_expect.md: -------------------------------------------------------------------------------- 1 | # manual_expect 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/manual/manual_expect.rs#L46) 6 | 7 | ## What it does 8 | 9 | Checks for manual implementations of `expect`. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let foo: Option:: = Option::None; 16 | let _foo = match foo { 17 | Option::Some(x) => x, 18 | Option::None => core::panic_with_felt252('err'), 19 | }; 20 | } 21 | ``` 22 | 23 | Can be rewritten as: 24 | 25 | ```cairo 26 | fn main() { 27 | let foo: Option:: = Option::None; 28 | let _foo = foo.expect('err'); 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /website/docs/lints/manual_err.md: -------------------------------------------------------------------------------- 1 | # manual_err 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/manual/manual_err.rs#L41) 6 | 7 | ## What it does 8 | 9 | Checks for manual implementations of `err` in match and if expressions. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let foo: Result = Result::Err('err'); 16 | let _foo = match foo { 17 | Result::Ok(_) => Option::None, 18 | Result::Err(x) => Option::Some(x), 19 | }; 20 | } 21 | ``` 22 | 23 | Can be rewritten as: 24 | 25 | ```cairo 26 | fn main() { 27 | let foo: Result = Result::Err('err'); 28 | let _foo = foo.err(); 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /website/docs/lints/manual_ok_or.md: -------------------------------------------------------------------------------- 1 | # manual_ok_or 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/manual/manual_ok_or.rs#L52) 6 | 7 | ## What it does 8 | 9 | Checks for manual implementations of ok_or. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let foo: Option = Option::None; 16 | let _foo = match foo { 17 | Option::Some(v) => Result::Ok(v), 18 | Option::None => Result::Err('this is an err'), 19 | }; 20 | } 21 | ``` 22 | 23 | Can be rewritten as: 24 | 25 | ```cairo 26 | fn main() { 27 | let foo: Option = Option::None; 28 | let _foo = foo.ok_or('this is an err'); 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /website/docs/lints/redundant_brackets_in_enum_call.md: -------------------------------------------------------------------------------- 1 | # redundant_brackets_in_enum_call 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/redundant_brackets_in_enum_call.rs#L47) 6 | 7 | ## What it does 8 | 9 | Detects calls to enum variant constructors with redundant parentheses 10 | 11 | ## Example 12 | 13 | ```cairo 14 | enum MyEnum { 15 | Data: u8, 16 | Empty, 17 | } 18 | 19 | fn main() { 20 | let a = MyEnum::Empty(()); // redundant parentheses 21 | } 22 | ``` 23 | 24 | Can be simplified to: 25 | 26 | ```cairo 27 | enum MyEnum { 28 | Data: u8, 29 | Empty, 30 | } 31 | 32 | fn main() { 33 | let a = MyEnum::Empty; 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /website/docs/lints/loop_match_pop_front.md: -------------------------------------------------------------------------------- 1 | # loop_match_pop_front 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/loops/loop_match_pop_front.rs#L53) 6 | 7 | ## What it does 8 | 9 | Checks for loops that are used to iterate over a span using `pop_front`. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | let a: Span = array![1, 2, 3].span(); 15 | loop { 16 | match a.pop_front() { 17 | Option::Some(val) => {do_smth(val); }, 18 | Option::None => { break; } 19 | } 20 | } 21 | ``` 22 | 23 | Which can be rewritten as 24 | 25 | ```cairo 26 | let a: Span = array![1, 2, 3].span(); 27 | for val in a { 28 | do_smth(val); 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /website/docs/lints/manual_ok.md: -------------------------------------------------------------------------------- 1 | # manual_ok 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/manual/manual_ok.rs#L41) 6 | 7 | ## What it does 8 | 9 | Checks for manual implementation of `ok` method in match and if expressions. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let res_val: Result = Result::Err('err'); 16 | let _a = match res_val { 17 | Result::Ok(x) => Option::Some(x), 18 | Result::Err(_) => Option::None, 19 | }; 20 | } 21 | ``` 22 | 23 | Can be replaced with: 24 | 25 | ```cairo 26 | fn main() { 27 | let res_val: Result = Result::Err('err'); 28 | let _a = res_val.ok(); 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cairo-lint-website", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vitepress dev", 7 | "build": "vitepress build", 8 | "clean": "rm -rf .vitepress/cache .vitepress/dist", 9 | "fmt": "prettier --write .", 10 | "fmt:check": "prettier --check .", 11 | "preview": "vitepress preview" 12 | }, 13 | "dependencies": { 14 | "cairo-tm-grammar": "github:software-mansion-labs/cairo-tm-grammar", 15 | "prettier": "3.3.2", 16 | "vitepress": "1.2.3", 17 | "vue": "3.4.30" 18 | }, 19 | "optionalDependencies": { 20 | "@rollup/rollup-linux-x64-gnu": "4.9.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /website/docs/lints/contradictory_comparison.md: -------------------------------------------------------------------------------- 1 | # contradictory_comparison 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/double_comparison.rs#L188) 6 | 7 | ## What it does 8 | 9 | Checks for double comparisons that are contradictory. Those are comparisons that are always false. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() -> bool { 15 | let x = 5_u32; 16 | let y = 10_u32; 17 | if x < y && x > y { 18 | true 19 | } else { 20 | false 21 | } 22 | } 23 | ``` 24 | 25 | Could be simplified to just: 26 | 27 | ```cairo 28 | fn main() -> bool { 29 | let x = 5_u32; 30 | let y = 10_u32; 31 | false 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /website/docs/lints/redundant_comparison.md: -------------------------------------------------------------------------------- 1 | # redundant_comparison 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/double_comparison.rs#L137) 6 | 7 | ## What it does 8 | 9 | Checks for double comparisons that are redundant. Those are comparisons that can be simplified to a single comparison. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() -> bool { 15 | let x = 5_u32; 16 | let y = 10_u32; 17 | if x >= y || x <= y { 18 | true 19 | } else { 20 | false 21 | } 22 | } 23 | ``` 24 | 25 | Could be simplified to just: 26 | 27 | ```cairo 28 | fn main() -> bool { 29 | let x = 5_u32; 30 | let y = 10_u32; 31 | true 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cairo lint 2 | 3 | A collection of lints to catch common mistakes and improve your [Cairo](https://github.com/starkware-libs/cairo) code. 4 | 5 | ## Quick start 6 | 7 | Cairo-lint is a library that can detect and fix the detected problems. 8 | 9 | It it also shipped as a CLI with Scarb. To learn more visit [scarb lint](https://docs.swmansion.com/scarb/docs/extensions/linter.html) docs. 10 | 11 | ## Features 12 | 13 | - The `--test` flag to include test files. 14 | 15 | ## Documentation 16 | 17 | All information about Cairo lint is available on project's [website](https://docs.swmansion.com/cairo-lint/). 18 | 19 | ## Community 20 | 21 | As for now there is only a [telegram channel](https://t.me/cairolint) dedicated to cairo-lint. 22 | -------------------------------------------------------------------------------- /website/docs/lints/ifs_same_cond.md: -------------------------------------------------------------------------------- 1 | # ifs_same_cond 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/ifs/ifs_same_cond.rs#L43) 6 | 7 | ## What it does 8 | 9 | Checks for consecutive `if` expressions with the same condition. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let a = 1; 16 | let b = 1; 17 | if a == b { 18 | println!("a is equal to b"); 19 | } else if a == b { 20 | println!("a is equal to b"); 21 | } 22 | } 23 | ``` 24 | 25 | Could be rewritten as just: 26 | 27 | ```cairo 28 | fn main() { 29 | let a = 1; 30 | let b = 1; 31 | if a == b { 32 | println!("a is equal to b"); 33 | } 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /website/docs/lints/loop_for_while.md: -------------------------------------------------------------------------------- 1 | # loop_for_while 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/loops/loop_for_while.rs#L49) 6 | 7 | ## What it does 8 | 9 | Checks for `loop` expressions that contain a conditional `if` statement with break inside that 10 | can be simplified to a `while` loop. 11 | 12 | ## Example 13 | 14 | ```cairo 15 | fn main() { 16 | let mut x: u16 = 0; 17 | loop { 18 | if x == 10 { 19 | break; 20 | } 21 | x += 1; 22 | } 23 | } 24 | ``` 25 | 26 | Can be simplified to: 27 | 28 | ```cairo 29 | fn main() { 30 | let mut x: u16 = 0; 31 | while x != 10 { 32 | x += 1; 33 | } 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /website/.vitepress/components/home/Download.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 35 | -------------------------------------------------------------------------------- /website/docs/lints/collapsible_if.md: -------------------------------------------------------------------------------- 1 | # collapsible_if 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/ifs/collapsible_if.rs#L52) 6 | 7 | ## What it does 8 | 9 | Checks for nested `if` statements that can be collapsed into a single `if` statement. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let x = true; 16 | let y = true; 17 | let z = false; 18 | 19 | if x || z { 20 | if y && z { 21 | println!("Hello"); 22 | } 23 | } 24 | } 25 | ``` 26 | 27 | Can be collapsed to 28 | 29 | ```cairo 30 | fn main() { 31 | let x = true; 32 | let y = true; 33 | let z = false; 34 | if (x || z) && (y && z) { 35 | println!("Hello"); 36 | } 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /website/docs/lints/manual_expect_err.md: -------------------------------------------------------------------------------- 1 | # manual_expect_err 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/manual/manual_expect_err.rs#L54) 6 | 7 | ## What it does 8 | 9 | Checks for manual implementation of `expect_err` method in match and if expressions. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | fn main() { 15 | let foo: Result = Result::Err('err'); 16 | let err = 'this is an err'; 17 | let _foo = match foo { 18 | Result::Ok(_) => core::panic_with_felt252(err), 19 | Result::Err(x) => x, 20 | }; 21 | } 22 | ``` 23 | 24 | Can be rewritten as: 25 | 26 | ```cairo 27 | fn main() { 28 | let foo: Result = Result::Err('err'); 29 | let err = 'this is an err'; 30 | let _foo = foo.expect_err(err); 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /website/docs/lints/simplifiable_comparison.md: -------------------------------------------------------------------------------- 1 | # simplifiable_comparison 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/double_comparison.rs#L86) 6 | 7 | ## What it does 8 | 9 | Checks for double comparisons that can be simplified. 10 | Those are comparisons that can be simplified to a single comparison. 11 | 12 | ## Example 13 | 14 | ```cairo 15 | fn main() -> bool { 16 | let x = 5_u32; 17 | let y = 10_u32; 18 | if x == y || x > y { 19 | true 20 | } else { 21 | false 22 | } 23 | } 24 | ``` 25 | 26 | The above code can be simplified to: 27 | 28 | ```cairo 29 | fn main() -> bool { 30 | let x = 5_u32; 31 | let y = 10_u32; 32 | if x >= y { 33 | true 34 | } else { 35 | false 36 | } 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest release is supported with security updates. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | If there are any vulnerabilities in **Cairo language linter**, don't hesitate to 10 | _report them_. 11 | 12 | 1. If you found a vulnerability in **Scarb**, please consult its 13 | own [security policy](https://github.com/software-mansion/scarb/security/policy). 14 | 2. Use the GitHub Security site for reporting vulnerabilities. You can report 15 | one [here](https://github.com/software-mansion/cairo-lint/security/advisories/new). 16 | 3. Please **do not disclose the vulnerability publicly** until a fix is released! 17 | 4. Once we have either: a) published a fix or b) declined to address the vulnerability for whatever 18 | reason, you are free to publicly disclose it. 19 | -------------------------------------------------------------------------------- /website/docs/lints/collapsible_if_else.md: -------------------------------------------------------------------------------- 1 | # collapsible_if_else 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/ifs/collapsible_if_else.rs#L54) 6 | 7 | ## What it does 8 | 9 | Checks for nested `if` statements inside the `else` statement 10 | that can be collapsed into a single `if-else` statement. 11 | 12 | ## Example 13 | 14 | ```cairo 15 | fn main() { 16 | let x = true; 17 | if x { 18 | println!("x is true"); 19 | } else { 20 | if !x { 21 | println!("x is false"); 22 | } 23 | } 24 | } 25 | ``` 26 | 27 | Can be refactored to: 28 | 29 | ```cairo 30 | fn main() { 31 | let x = true; 32 | if x { 33 | println!("x is true"); 34 | } else if !x { 35 | println!("x is false"); 36 | } 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | 4 | macro_rules! command { 5 | ($enum_name:ident ( $($module:ident,)+ )) => { 6 | $(mod $module;)+ 7 | 8 | #[derive(::clap::Subcommand)] 9 | #[allow(non_camel_case_types)] 10 | enum $enum_name { 11 | $($module(crate::$module::Args),)+ 12 | } 13 | 14 | impl $enum_name { 15 | fn main(self) -> ::anyhow::Result<()> { 16 | match self { 17 | $(Self::$module(args) => crate::$module::main(args),)+ 18 | } 19 | } 20 | } 21 | } 22 | } 23 | 24 | command!(Command(upgrade, sync_version, update_docs,)); 25 | 26 | #[derive(Parser)] 27 | struct Args { 28 | #[command(subcommand)] 29 | command: Command, 30 | } 31 | 32 | fn main() -> Result<()> { 33 | Args::parse().command.main() 34 | } 35 | -------------------------------------------------------------------------------- /tests/main.rs: -------------------------------------------------------------------------------- 1 | mod bitwise_for_parity_check; 2 | mod bool_comparison; 3 | mod breaks; 4 | mod clone_on_copy; 5 | mod collapsible_match; 6 | mod double_comparison; 7 | mod double_parens; 8 | mod duplicate_underscore_args; 9 | mod empty_enum_brackets_variant; 10 | mod enum_variant_names; 11 | mod eq_op; 12 | mod erasing_operations; 13 | mod fix_messages; 14 | mod helpers; 15 | mod ifs; 16 | mod int_operations; 17 | mod loops; 18 | mod manual; 19 | mod nested_fixes; 20 | mod panic; 21 | mod performance; 22 | mod redundant_brackets_in_enum_call; 23 | mod redundant_into; 24 | mod redundant_op; 25 | mod single_match; 26 | mod unit_return_type; 27 | mod unused_imports; 28 | mod unused_variables; 29 | mod unwrap_syscall; 30 | 31 | pub const CRATE_CONFIG: &str = r#" 32 | edition = "2024_07" 33 | 34 | [experimental_features] 35 | negative_impls = true 36 | coupons = true 37 | associated_item_constraints = true 38 | user_defined_inline_macros = true 39 | "#; 40 | -------------------------------------------------------------------------------- /website/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | *.lcov 2 | *.log 3 | *.pid 4 | *.pid.lock 5 | *.seed 6 | *.tgz 7 | *.tsbuildinfo 8 | .cache 9 | .cache/ 10 | .docusaurus 11 | .dynamodb/ 12 | .env 13 | .env.development.local 14 | .env.local 15 | .env.production.local 16 | .env.test.local 17 | .eslintcache 18 | .fusebox/ 19 | .grunt 20 | .lock-wscript 21 | .next 22 | .node_repl_history 23 | .npm 24 | .nuxt 25 | .nyc_output 26 | .parcel-cache 27 | .pnp.* 28 | .pnpm-debug.log* 29 | .rpt2_cache/ 30 | .rts2_cache_cjs/ 31 | .rts2_cache_es/ 32 | .rts2_cache_umd/ 33 | .serverless/ 34 | .stylelintcache 35 | .temp 36 | .tern-port 37 | .vitepress/cache 38 | .vscode-test 39 | .vuepress/dist 40 | .yarn-integrity 41 | .yarn/build-state.yml 42 | .yarn/cache 43 | .yarn/install-state.gz 44 | .yarn/unplugged 45 | bower_components 46 | build/Release 47 | coverage 48 | dist 49 | jspm_packages/ 50 | lerna-debug.log* 51 | lib-cov 52 | logs 53 | node_modules/ 54 | npm-debug.log* 55 | out 56 | pids 57 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 58 | web_modules/ 59 | yarn-debug.log* 60 | yarn-error.log* 61 | -------------------------------------------------------------------------------- /MAINTAINING.md: -------------------------------------------------------------------------------- 1 | # Cairo-lint Maintenance 2 | 3 | ## Release procedure 4 | 5 | To release a new version of `cairo-lint`: 6 | 7 | 1. Bump `cairo-lint` version in `Cargo.toml` file. 8 | 2. Make sure all the `cairo-lang-*` dependencies are set to a version appropriate for your release. 9 | You can use the following command: 10 | ```shell 11 | cargo xtask upgrade cairo VERSION 12 | ``` 13 | where `VERSION` is the appropriate version. 14 | 15 | The `patch` section in `Cargo.toml` should be **empty** after doing it. 16 | 3. Push the changes, create a PR and verify if the CI passes. 17 | 4. If releasing for the first time, run: 18 | ```shell 19 | cargo login 20 | ``` 21 | and follow terminal prompts to generate a token with at least `publish-update` permission. 22 | 5. Run 23 | ```shell 24 | cargo publish 25 | ``` 26 | OR (if using multiple tokens for multiple crates): 27 | ```shell 28 | cargo publish --token 29 | ``` 30 | to publish the package. 31 | 6. Create a tag corresponding to the release. The naming convention is `v{VERSION}` e.g. `v2.12.0`. 32 | -------------------------------------------------------------------------------- /src/plugin.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::ids::ModuleId; 2 | use cairo_lang_defs::plugin::PluginDiagnostic; 3 | use cairo_lang_semantic::plugin::{AnalyzerPlugin, PluginSuite}; 4 | use salsa::Database; 5 | 6 | use crate::context::get_unique_allowed_names; 7 | 8 | pub fn cairo_lint_allow_plugin_suite() -> PluginSuite { 9 | let mut suite = PluginSuite::default(); 10 | suite.add_analyzer_plugin::(); 11 | suite 12 | } 13 | 14 | /// Plugin with `declared_allows` that does not emit diagnostics. 15 | /// Add it to avoid compiler warnings on unsupported `allow` attribute arguments. 16 | #[derive(Debug, Default)] 17 | pub struct CairoLintAllow; 18 | 19 | impl AnalyzerPlugin for CairoLintAllow { 20 | fn diagnostics<'db>( 21 | &self, 22 | _db: &'db dyn Database, 23 | _module_id: ModuleId<'db>, 24 | ) -> Vec> { 25 | Vec::new() 26 | } 27 | 28 | fn declared_allows(&self) -> Vec { 29 | get_unique_allowed_names() 30 | .iter() 31 | .map(ToString::to_string) 32 | .collect() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for enhancing Cairo language linter. 3 | type: Feature 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for filing a 🙋 feature request! 😄 9 | 10 | In case of a relatively big feature that needs feedback from the community, [the cairo-lint Telegram channel][tg] is the best fit. 11 | 12 | [tg]: https://t.me/cairolint 13 | - type: textarea 14 | id: problem 15 | attributes: 16 | label: Problem 17 | description: > 18 | Please provide a clear description of your use case and the problem 19 | this feature request is trying to solve. 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: solution 24 | attributes: 25 | label: Proposed Solution 26 | description: > 27 | Please provide a clear and concise description of what you want to happen. 28 | - type: textarea 29 | id: notes 30 | attributes: 31 | label: Notes 32 | description: Provide any additional context or information that might be helpful. 33 | -------------------------------------------------------------------------------- /website/docs/lints/unwrap_syscall.md: -------------------------------------------------------------------------------- 1 | # unwrap_syscall 2 | 3 | Default: **Enabled** 4 | 5 | [Source Code](https://github.com/software-mansion/cairo-lint/tree/main/src/lints/unwrap_syscall.rs#L52) 6 | 7 | ## What it does 8 | 9 | Detects if the function uses `unwrap` on a `SyscallResult` object. 10 | 11 | ## Example 12 | 13 | ```cairo 14 | use starknet::storage_access::{storage_address_from_base, storage_base_address_from_felt252}; 15 | use starknet::syscalls::storage_read_syscall; 16 | 17 | fn main() { 18 | let storage_address = storage_base_address_from_felt252(3534535754756246375475423547453); 19 | let result = storage_read_syscall(0, storage_address_from_base(storage_address)); 20 | result.unwrap(); 21 | } 22 | ``` 23 | 24 | Can be changed to: 25 | 26 | ```cairo 27 | use starknet::SyscallResultTrait; 28 | use starknet::storage_access::{storage_address_from_base, storage_base_address_from_felt252}; 29 | use starknet::syscalls::storage_read_syscall; 30 | 31 | fn main() { 32 | let storage_address = storage_base_address_from_felt252(3534535754756246375475423547453); 33 | let result = storage_read_syscall(0, storage_address_from_base(storage_address)); 34 | result.unwrap_syscall(); 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /tests/fix_messages/mod.rs: -------------------------------------------------------------------------------- 1 | use cairo_lint::context::get_all_fix_messages; 2 | 3 | #[test] 4 | fn check_fix_message() { 5 | let fix_messages = get_all_fix_messages(); 6 | 7 | // Ensure all lints with a fixer have a non-empty fix_message 8 | for msg in fix_messages { 9 | assert!( 10 | msg.is_some(), 11 | "Every lint with `has_fixer` must provide a `fix_message`" 12 | ); 13 | assert!( 14 | !msg.unwrap().is_empty(), 15 | "`fix_message` should not be an empty string" 16 | ); 17 | } 18 | } 19 | 20 | #[test] 21 | #[should_panic(expected = "`fix_message` should not be an empty string")] 22 | fn test_empty_fix_message_panics() { 23 | let mut fix_messages = get_all_fix_messages(); 24 | 25 | // Simulate a lint with an empty fix_message (for testing purposes) 26 | let empty_fix_message: Option<&'static str> = Some(""); 27 | fix_messages.push(empty_fix_message); 28 | 29 | // Trigger the assertion that checks for non-empty messages 30 | for msg in fix_messages { 31 | assert!( 32 | msg.is_some(), 33 | "Every lint with `has_fixer` must provide a `fix_message`" 34 | ); 35 | assert!( 36 | !msg.unwrap().is_empty(), 37 | "`fix_message` should not be an empty string" 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * 1,3,5" 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | test-with-nightly-cairo: 13 | name: Test nightly 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v6 17 | - uses: dtolnay/rust-toolchain@stable 18 | 19 | - name: Upgrade Cairo to latest main commit 20 | run: cargo xtask upgrade cairo --rev $(git ls-remote --refs "https://github.com/starkware-libs/cairo" main | awk '{print $1}') 21 | - name: Get corelib 22 | run: git clone https://github.com/starkware-libs/cairo 23 | - name: Run cargo test 24 | run: CORELIB_PATH="$(pwd)/cairo/corelib/src" cargo test 25 | 26 | notify_failed: 27 | runs-on: ubuntu-latest 28 | if: always() && needs.test-with-nightly-cairo.result == 'failure' && github.event_name == 'schedule' 29 | needs: test-with-nightly-cairo 30 | steps: 31 | - name: Notify the team about workflow failure 32 | uses: slackapi/slack-github-action@v2.1.1 33 | with: 34 | webhook: ${{ secrets.SLACK_NIGHTLY_CHECK_FAILURE_WEBHOOK_URL }} 35 | webhook-type: webhook-trigger 36 | payload: | 37 | url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help us improve 3 | type: Bug 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: Thanks for filing a 🐛 bug report 😄! 8 | - type: textarea 9 | id: problem 10 | attributes: 11 | label: Problem 12 | description: > 13 | Please provide a clear and concise description of what the bug is, 14 | including what currently happens and what you expected to happen. 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: steps 19 | attributes: 20 | label: Steps 21 | description: Please list the steps to reproduce the bug. 22 | placeholder: | 23 | 1. 24 | 2. 25 | 3. 26 | - type: textarea 27 | id: scarb-version 28 | attributes: 29 | label: Scarb Version 30 | description: > 31 | Please paste the output of running `scarb --version`. 32 | Remember to do this in the affected project directory. 33 | render: text 34 | validations: 35 | required: true 36 | - type: textarea 37 | id: possible-solutions 38 | attributes: 39 | label: Possible Solution(s) 40 | description: > 41 | Not obligatory, but suggest a fix/reason for the bug, 42 | or ideas how to implement the addition or change. 43 | - type: textarea 44 | id: notes 45 | attributes: 46 | label: Notes 47 | description: Provide any additional notes that might be helpful. 48 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Cairo Lint Roadmap 2 | 3 | A list of main features and tasks that are planned to be added for Cairo lint. 4 | 5 | The list is sorted from top to bottom by the priority that each task has. The higher the priority, the higher the place of the task in the list. 6 | Size of the task is ranked `1-5`, where `1` is smallest and `5` biggest. 7 | 8 | For more detailed info, look into the [Board](https://github.com/orgs/software-mansion/projects/33/views/7) into the `Backlog Lint` and `Todo` sections. 9 | 10 | ## Q4 2025 and Q1 2026 11 | 12 | ### 1. Fixing any existing bugs 13 | 14 | We still have some bugs to be addressed. Most of them are related to code produced by inline macros being considered as a user code. Also other minor bugs related to lints such as `unwrap_or_else`. 15 | 16 | Size: 4 17 | 18 | ### 2. Upstreaming shared logic with Cairo Language Server 19 | 20 | A lot of the critical code parts are just blatantly copied from Language Server. We want to create a separate crate that will contain all the shared logic between those two. This way we get a single source of truth, and it will be much easier to maintain. 21 | 22 | Size: 3 23 | 24 | ### 3. Add various new lints and fixers 25 | 26 | We want to add new lint rules (and corresponding fixer if possible), such as `collapsible_match`, `disallowed_methods`, `inefficient_unwrap_or`, `impossible_comparison` and much more. The list of lints waiting to be implemented is very long, but those mentioned should be the priority during this period. 27 | 28 | Size: 5 29 | -------------------------------------------------------------------------------- /tests/helpers/setup.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_filesystem::db::{CrateConfigurationInput, CrateSettings, files_group_input}; 2 | use cairo_lang_filesystem::{ 3 | db::{Edition, ExperimentalFeaturesConfig}, 4 | ids::FileKind, 5 | }; 6 | use cairo_lang_utils::ordered_hash_map::OrderedHashMap; 7 | use std::collections::BTreeMap; 8 | 9 | use crate::CRATE_CONFIG; 10 | use cairo_lang_filesystem::ids::{CrateInput, DirectoryInput, FileInput, VirtualFileInput}; 11 | use cairo_lint::LinterAnalysisDatabase; 12 | use salsa::Setter; 13 | 14 | pub fn setup_test_crate_ex(db: &mut LinterAnalysisDatabase, content: &str) -> CrateInput { 15 | let settings = CrateSettings { 16 | name: None, 17 | edition: Edition::latest(), 18 | version: None, 19 | dependencies: Default::default(), 20 | experimental_features: ExperimentalFeaturesConfig { 21 | negative_impls: true, 22 | associated_item_constraints: true, 23 | coupons: true, 24 | user_defined_inline_macros: true, 25 | repr_ptrs: true, 26 | }, 27 | cfg_set: Default::default(), 28 | }; 29 | let file = FileInput::Virtual(VirtualFileInput { 30 | parent: None, 31 | name: "lib.cairo".into(), 32 | content: content.into(), 33 | code_mappings: [].into(), 34 | kind: FileKind::Module, 35 | original_item_removed: false, 36 | }); 37 | 38 | let cr = CrateInput::Virtual { 39 | name: "test".into(), 40 | file_long_id: file.clone(), 41 | settings: CRATE_CONFIG.to_string(), 42 | cache_file: None, 43 | }; 44 | 45 | files_group_input(db) 46 | .set_crate_configs(db) 47 | .to(Some(OrderedHashMap::from([( 48 | cr.clone(), 49 | CrateConfigurationInput { 50 | root: DirectoryInput::Virtual { 51 | files: BTreeMap::from([("lib.cairo".to_string(), file)]), 52 | dirs: Default::default(), 53 | }, 54 | settings, 55 | cache_file: None, 56 | }, 57 | )]))); 58 | 59 | cr 60 | } 61 | -------------------------------------------------------------------------------- /website/docs.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Cairo lint is a static code analysis tool for the Cairo language. 4 | 5 | It can help you improve your code quality and consistency by checking the codebase against a set of predefined rules, called lints. 6 | It can also automatically fix some of the issues found. 7 | 8 | This tool is mostly depended on the separate lint rules. You can also read about every each of them here in the `Lints` section of the documentation. 9 | 10 | ## Installation 11 | 12 | Cairo lint is provided within the [Scarb](https://docs.swmansion.com/scarb/) toolchain. You can install and download it [here](https://docs.swmansion.com/scarb/download.html) 13 | 14 | ## Getting started 15 | 16 | To run Cairo lint in the current project, just type: 17 | 18 | ```sh 19 | scarb lint 20 | ``` 21 | 22 | This will run the code analysis and suggest places to edit your code. 23 | Running `lint` will yield issues like this: 24 | 25 | ```sh 26 | $ scarb lint 27 | Linting hello_world v0.1.0 (/hello_world/Scarb.toml) 28 | warning: Plugin diagnostic: Unnecessary comparison with a boolean value. Use the variable directly. 29 | --> /hello_world/src/lib.cairo:2:8 30 | | 31 | 2 | if is_true() == true { 32 | | ----------------- 33 | | 34 | ``` 35 | 36 | To attempt to fix the issues automatically, you can run: 37 | 38 | ```sh 39 | scarb lint --fix 40 | ``` 41 | 42 | You can also specify `--test` to perform analysis of your project's tests as well (i.e. all the Cairo code under `#[cfg(test)]` attributes). 43 | To learn more about available arguments, just run `scarb lint --help`. 44 | 45 | ## Configuration 46 | 47 | By default, most of the lints are enabled and checked, but some of them are disabled as they are much more pedantic. You can check whether a certain lint is enabled by default in the documentation. To adjust this configuration, you can manually set those values in your `Scarb.toml` as follows: 48 | 49 | ```toml 50 | [tool.cairo-lint] 51 | panic = true 52 | bool_comparison = false 53 | ``` 54 | 55 | This example config will enable a `panic` checking lint (which is disabled by default), and disable a `bool_comparison` lint (which is enabled by default). 56 | -------------------------------------------------------------------------------- /.github/workflows/website-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Website Deploy 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | target_branch: 7 | description: "Git branch to deploy from" 8 | required: true 9 | type: string 10 | 11 | workflow_dispatch: 12 | inputs: 13 | target_branch: 14 | description: "Git branch to deploy from" 15 | type: string 16 | default: "main" 17 | 18 | permissions: 19 | contents: read 20 | pages: write 21 | id-token: write 22 | 23 | concurrency: 24 | group: "website-deploy" 25 | cancel-in-progress: true 26 | 27 | jobs: 28 | validate-branch: 29 | name: Validate target branch 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Set target ref 33 | run: | 34 | VERSION_REGEX="^v[0-9]+\.[0-9]+\.[0-9]+$" 35 | INPUT_BRANCH="${{ inputs.target_branch }}" 36 | 37 | # If the branch does not match the version regex, exit with failure. 38 | if [[ ! $INPUT_BRANCH =~ $VERSION_REGEX ]]; then 39 | exit 1 40 | fi 41 | build: 42 | name: Build website 43 | runs-on: ubuntu-latest 44 | needs: 45 | - validate-branch 46 | defaults: 47 | run: 48 | working-directory: ./website 49 | steps: 50 | - uses: actions/checkout@v6 51 | with: 52 | ref: ${{ inputs.target_ref }} 53 | - uses: dtolnay/rust-toolchain@master 54 | with: 55 | toolchain: nightly 56 | - uses: actions/setup-node@v6 57 | with: 58 | node-version: "22.x" 59 | - uses: actions/configure-pages@v5 60 | 61 | - name: Update docs 62 | working-directory: . 63 | run: cargo xtask update-docs 64 | - run: npm ci 65 | - run: npm run build 66 | - name: Upload GitHub Pages artifact 67 | uses: actions/upload-pages-artifact@v4 68 | with: 69 | path: website/.vitepress/dist 70 | 71 | deploy: 72 | environment: 73 | name: github-pages 74 | url: ${{ steps.deployment.outputs.page_url }} 75 | runs-on: ubuntu-latest 76 | needs: 77 | - validate-branch 78 | - build 79 | steps: 80 | - name: Deploy to GitHub Pages 81 | id: deployment 82 | uses: actions/deploy-pages@v4 83 | -------------------------------------------------------------------------------- /website/.vitepress/components/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 33 | 34 | 98 | -------------------------------------------------------------------------------- /src/lints/duplicate_underscore_args.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use crate::context::{CairoLintKind, Lint}; 4 | use cairo_lang_defs::{ids::ModuleItemId, plugin::PluginDiagnostic}; 5 | use cairo_lang_diagnostics::Severity; 6 | use cairo_lang_semantic::items::function_with_body::FunctionWithBodySemantic; 7 | 8 | use crate::queries::get_all_checkable_functions; 9 | use salsa::Database; 10 | 11 | pub struct DuplicateUnderscoreArgs; 12 | 13 | /// ## What it does 14 | /// 15 | /// Checks for functions that have the same argument name but prefix with `_`. 16 | /// 17 | /// ## Example 18 | /// 19 | /// This code will raise a warning because it can be difficult to differentiate between `test` and `_test`. 20 | /// 21 | /// ```cairo 22 | /// fn foo(test: u32, _test: u32) {} 23 | /// ``` 24 | impl Lint for DuplicateUnderscoreArgs { 25 | fn allowed_name(&self) -> &'static str { 26 | "duplicate_underscore_args" 27 | } 28 | 29 | fn diagnostic_message(&self) -> &'static str { 30 | "duplicate arguments, having another argument having almost the same name \ 31 | makes code comprehension and documentation more difficult" 32 | } 33 | 34 | fn kind(&self) -> CairoLintKind { 35 | CairoLintKind::DuplicateUnderscoreArgs 36 | } 37 | } 38 | 39 | #[tracing::instrument(skip_all, level = "trace")] 40 | pub fn check_duplicate_underscore_args<'db>( 41 | db: &'db dyn Database, 42 | item: &ModuleItemId<'db>, 43 | diagnostics: &mut Vec>, 44 | ) { 45 | let functions = get_all_checkable_functions(db, item); 46 | 47 | for function in functions { 48 | let mut registered_names: HashSet = HashSet::new(); 49 | let params = db 50 | .function_with_body_signature(function) 51 | .cloned() 52 | .unwrap() 53 | .params; 54 | 55 | for param in params { 56 | let name_string = param.name.to_string(db); 57 | let stripped_name = name_string.strip_prefix('_').unwrap_or(&name_string); 58 | 59 | if !registered_names.insert(stripped_name.to_string()) { 60 | diagnostics.push(PluginDiagnostic { 61 | stable_ptr: param.stable_ptr.0, 62 | message: DuplicateUnderscoreArgs.diagnostic_message().to_string(), 63 | severity: Severity::Warning, 64 | inner_span: None, 65 | }); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/mappings.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::ids::{LanguageElementId, ModuleItemId}; 2 | use cairo_lang_filesystem::{db::get_originating_location, ids::SpanInFile}; 3 | use cairo_lang_syntax::node::{SyntaxNode, ast::ModuleItem, ids::SyntaxStablePtrId}; 4 | use cairo_language_common::CommonGroup; 5 | use salsa::Database; 6 | 7 | #[tracing::instrument(level = "trace", skip(db))] 8 | pub fn get_origin_module_item_as_syntax_node<'db>( 9 | db: &'db dyn Database, 10 | module_item_id: &ModuleItemId<'db>, 11 | ) -> Option> { 12 | let ptr = module_item_id.stable_location(db).stable_ptr(); 13 | let SpanInFile { file_id, span } = get_originating_location( 14 | db, 15 | SpanInFile { 16 | file_id: ptr.file_id(db), 17 | span: ptr.lookup(db).span_without_trivia(db), 18 | }, 19 | None, 20 | ); 21 | 22 | db.find_syntax_node_at_offset(file_id, span.start)? 23 | .ancestors_with_self(db) 24 | .find(|n| ModuleItem::is_variant(n.kind(db))) 25 | } 26 | 27 | /// Returns the originating syntax node for a given stable pointer. 28 | #[tracing::instrument(level = "trace", skip(db))] 29 | pub fn get_origin_syntax_node<'db>( 30 | db: &'db dyn Database, 31 | ptr: &SyntaxStablePtrId<'db>, 32 | ) -> Option> { 33 | let syntax_node = ptr.lookup(db); 34 | let SpanInFile { file_id, span } = get_originating_location( 35 | db, 36 | SpanInFile { 37 | file_id: ptr.file_id(db), 38 | span: ptr.lookup(db).span_without_trivia(db), 39 | }, 40 | None, 41 | ); 42 | 43 | // Heuristically find the syntax node at the given offset. 44 | // We match the ancestors with node text to ensure we get the whole node. 45 | return db 46 | .find_syntax_node_at_offset(file_id, span.start)? 47 | .ancestors_with_self(db) 48 | .find(|node| node.get_text_without_trivia(db) == syntax_node.get_text_without_trivia(db)); 49 | } 50 | 51 | #[tracing::instrument(level = "trace", skip(db))] 52 | pub fn get_originating_syntax_node_for<'db>( 53 | db: &'db dyn Database, 54 | ptr: &SyntaxStablePtrId<'db>, 55 | ) -> Option> { 56 | let SpanInFile { file_id, span } = get_originating_location( 57 | db, 58 | SpanInFile { 59 | file_id: ptr.file_id(db), 60 | span: ptr.lookup(db).span_without_trivia(db), 61 | }, 62 | None, 63 | ); 64 | 65 | db.find_syntax_node_at_offset(file_id, span.start) 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v[0-9]+.[0-9]+.[0-9]+" 9 | pull_request: 10 | merge_group: 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | checks: 17 | name: Checks 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: dtolnay/rust-toolchain@stable 22 | - uses: Swatinem/rust-cache@v2 23 | - run: cargo fmt --all --check 24 | - run: cargo lint 25 | env: 26 | RUSTFLAGS: "-Dwarnings" 27 | - run: cargo docs 28 | env: 29 | RUSTDOCFLAGS: "-Dwarnings" 30 | - uses: taiki-e/install-action@cargo-machete 31 | - run: cargo machete 32 | build-test: 33 | name: Build tests 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v6 37 | - uses: dtolnay/rust-toolchain@stable 38 | - uses: Swatinem/rust-cache@v2 39 | - uses: taiki-e/install-action@nextest 40 | - name: Run nextest archive 41 | run: cargo nextest archive --workspace --all-features --cargo-profile ci --archive-file 'nextest-archive.tar.zst' 42 | - uses: actions/upload-artifact@v5 43 | with: 44 | name: nextest-archive 45 | path: nextest-archive.tar.zst 46 | test: 47 | name: Test ${{ matrix.partition}}/4 48 | runs-on: ubuntu-latest 49 | needs: 50 | - build-test 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | partition: [1, 2, 3, 4] 55 | steps: 56 | - uses: actions/checkout@v6 57 | - uses: dtolnay/rust-toolchain@stable 58 | - uses: software-mansion/setup-scarb@v1 59 | with: 60 | cache: false 61 | - uses: taiki-e/install-action@nextest 62 | - uses: actions/download-artifact@v6 63 | with: 64 | name: nextest-archive 65 | - name: Run nextest partition ${{ matrix.partition }}/4 66 | run: cargo nextest run --partition 'count:${{ matrix.partition }}/4' --archive-file 'nextest-archive.tar.zst' 67 | check-website: 68 | name: Website prettier check 69 | runs-on: ubuntu-latest 70 | defaults: 71 | run: 72 | working-directory: ./website 73 | steps: 74 | - uses: actions/checkout@v6 75 | - uses: actions/setup-node@v6 76 | with: 77 | node-version: "18.x" 78 | cache: npm 79 | cache-dependency-path: website/package-lock.json 80 | - run: npm ci 81 | - run: npm run fmt:check 82 | -------------------------------------------------------------------------------- /tests/redundant_op/mod.rs: -------------------------------------------------------------------------------- 1 | use super::{test_lint_diagnostics, test_lint_fixer}; 2 | 3 | const ADDITION_BY_ZERO: &str = r#" 4 | fn main() { 5 | let x = 42; 6 | let _y = x + 0; 7 | } 8 | "#; 9 | const SUBTRACTION_BY_ZERO: &str = r#" 10 | fn main() { 11 | let x = 42; 12 | let _y = x - 0; 13 | } 14 | "#; 15 | const MULTIPLICATION_BY_ONE: &str = r#" 16 | fn main() { 17 | let x = 42; 18 | let _y = x * 1; 19 | } 20 | "#; 21 | const DIVISION_BY_ONE: &str = r#" 22 | fn main() { 23 | let x = 42_u32; 24 | let _y = x / 1; 25 | } 26 | "#; 27 | 28 | #[test] 29 | fn addition_by_zero_diagnostics() { 30 | test_lint_diagnostics!(ADDITION_BY_ZERO, @r" 31 | Plugin diagnostic: This operation doesn't change the value and can be simplified. 32 | --> lib.cairo:4:14 33 | let _y = x + 0; 34 | ^^^^^ 35 | "); 36 | } 37 | #[test] 38 | fn addition_by_zero_fixer() { 39 | test_lint_fixer!(ADDITION_BY_ZERO, @r" 40 | fn main() { 41 | let x = 42; 42 | let _y = x + 0; 43 | } 44 | "); 45 | } 46 | #[test] 47 | fn subtraction_by_zero_diagnostics() { 48 | test_lint_diagnostics!(SUBTRACTION_BY_ZERO, @r" 49 | Plugin diagnostic: This operation doesn't change the value and can be simplified. 50 | --> lib.cairo:4:14 51 | let _y = x - 0; 52 | ^^^^^ 53 | "); 54 | } 55 | #[test] 56 | fn subtraction_by_zero_fixer() { 57 | test_lint_fixer!(SUBTRACTION_BY_ZERO, @r" 58 | fn main() { 59 | let x = 42; 60 | let _y = x - 0; 61 | } 62 | "); 63 | } 64 | #[test] 65 | fn multiplication_by_one_diagnostics() { 66 | test_lint_diagnostics!(MULTIPLICATION_BY_ONE, @r" 67 | Plugin diagnostic: This operation doesn't change the value and can be simplified. 68 | --> lib.cairo:4:14 69 | let _y = x * 1; 70 | ^^^^^ 71 | "); 72 | } 73 | #[test] 74 | fn multiplication_by_one_fixer() { 75 | test_lint_fixer!(MULTIPLICATION_BY_ONE, @r#" 76 | fn main() { 77 | let x = 42; 78 | let _y = x * 1; 79 | } 80 | "#); 81 | } 82 | #[test] 83 | fn division_by_one_diagnostics() { 84 | test_lint_diagnostics!(DIVISION_BY_ONE, @r" 85 | Plugin diagnostic: This operation doesn't change the value and can be simplified. 86 | --> lib.cairo:4:14 87 | let _y = x / 1; 88 | ^^^^^ 89 | "); 90 | } 91 | #[test] 92 | fn division_by_one_fixer() { 93 | test_lint_fixer!(DIVISION_BY_ONE, @r#" 94 | fn main() { 95 | let x = 42_u32; 96 | let _y = x / 1; 97 | } 98 | "#); 99 | } 100 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cairo-lint" 3 | description = "Core library for linting Cairo language code." 4 | version.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | license-file.workspace = true 8 | 9 | 10 | [workspace] 11 | resolver = "2" 12 | members = ["xtask"] 13 | 14 | [workspace.package] 15 | version = "2.15.0-rc.1" 16 | edition = "2024" 17 | repository = "https://github.com/software-mansion/cairo-lint" 18 | license-file = "LICENSE" 19 | 20 | # Managing dependencies on crates from starkware-libs/cairo repository: 21 | # 22 | # The Cairo compiler is made of a bunch of crates that inter-depend on each other and have 23 | # synchronised versioning. 24 | # It is very important to use a single revision of these crates in the entire Cairo toolchain, 25 | # which consists of Cairo compiler, Scarb, CairoLS and other tools. 26 | # The toolchain is eventually built by Scarb, which depends on everything other as regular crates. 27 | # To ensure that all crates in the toolchain use the same revision of Cairo crates, we use a patch 28 | # mechanism that Cargo provides. 29 | # Because Cargo requires patches to change the crate source, we have an unspoken contract that 30 | # all tools *always* depend on some crates.io versions of Cairo crates and Scarb uses 31 | # [patch.crates.io] table to set final git revision for everything. 32 | # 33 | # To keep our Cargo.toml following this contract, always use `cargo xtask upgrade` 34 | # for manipulating these dependencies. 35 | [dependencies] 36 | anyhow = "1.0.100" 37 | cairo-lang-compiler = "2.15.0-rc.1" 38 | cairo-lang-defs = "2.15.0-rc.1" 39 | cairo-lang-diagnostics = "2.15.0-rc.1" 40 | cairo-lang-filesystem = "2.15.0-rc.1" 41 | cairo-lang-formatter = "2.15.0-rc.1" 42 | cairo-lang-parser = "2.15.0-rc.1" 43 | cairo-lang-semantic = "2.15.0-rc.1" 44 | cairo-lang-syntax = "2.15.0-rc.1" 45 | cairo-lang-test-plugin = "2.15.0-rc.1" 46 | cairo-lang-utils = "2.15.0-rc.1" 47 | cairo-language-common = "2.15.0-rc.1" 48 | if_chain = "1.0.3" 49 | indoc = "2" 50 | itertools = "0.14.0" 51 | log = "0.4.28" 52 | num-bigint = "0.4.6" 53 | salsa = "0.24.0" 54 | scarb-metadata = "1.15.1" 55 | tempfile = "3" 56 | tracing = "0.1" 57 | which = "8" 58 | 59 | [dev-dependencies] 60 | ctor = "0.6.1" 61 | insta = "1.44.3" 62 | pretty_assertions = "1.4.1" 63 | test-case = "3.0" 64 | 65 | # Here we specify real dependency specifications for Cairo crates *if* currently we want to use 66 | # a particular unreleased commit (which is frequent mid-development). 67 | # We list all Cairo crates that go into Scarb's compilation unit even if Scarb itself does not depend 68 | # on some of them directly. 69 | # This ensures no duplicate instances of Cairo crates are pulled in by mistake. 70 | [patch.crates-io] 71 | 72 | [profile.ci] 73 | inherits = "test" 74 | strip = "debuginfo" 75 | [profile.ci.package."salsa"] 76 | opt-level = 3 77 | 78 | -------------------------------------------------------------------------------- /src/fixer/db.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::db::{defs_group_input, init_external_files}; 2 | use cairo_lang_filesystem::db::files_group_input; 3 | use cairo_lang_semantic::db::semantic_group_input; 4 | 5 | use salsa::{Database, Setter}; 6 | 7 | #[salsa::db] 8 | #[derive(Clone)] 9 | pub struct FixerDatabase { 10 | storage: salsa::Storage, 11 | } 12 | 13 | impl salsa::Database for FixerDatabase {} 14 | 15 | impl FixerDatabase { 16 | pub fn new_from(db: &dyn Database) -> Self { 17 | let mut new_db = Self::new(); 18 | // SemanticGroup salsa inputs. 19 | semantic_group_input(&new_db) 20 | .set_default_analyzer_plugins(&mut new_db) 21 | .to(semantic_group_input(db) 22 | .default_analyzer_plugins(db) 23 | .clone()); 24 | semantic_group_input(&new_db) 25 | .set_analyzer_plugin_overrides(&mut new_db) 26 | .to(semantic_group_input(db) 27 | .analyzer_plugin_overrides(db) 28 | .clone()); 29 | 30 | // DefsGroup salsa inputs. 31 | defs_group_input(&new_db) 32 | .set_default_macro_plugins(&mut new_db) 33 | .to(defs_group_input(db).default_macro_plugins(db).clone()); 34 | defs_group_input(&new_db) 35 | .set_macro_plugin_overrides(&mut new_db) 36 | .to(defs_group_input(db).macro_plugin_overrides(db).clone()); 37 | defs_group_input(&new_db) 38 | .set_default_inline_macro_plugins(&mut new_db) 39 | .to(defs_group_input(db) 40 | .default_inline_macro_plugins(db) 41 | .clone()); 42 | defs_group_input(&new_db) 43 | .set_inline_macro_plugin_overrides(&mut new_db) 44 | .to(defs_group_input(db) 45 | .inline_macro_plugin_overrides(db) 46 | .clone()); 47 | 48 | // FilesGroup salsa inputs. 49 | files_group_input(&new_db) 50 | .set_crate_configs(&mut new_db) 51 | .to(files_group_input(db).crate_configs(db).clone()); 52 | files_group_input(&new_db) 53 | .set_file_overrides(&mut new_db) 54 | .to(files_group_input(db).file_overrides(db).clone()); 55 | files_group_input(&new_db) 56 | .set_flags(&mut new_db) 57 | .to(files_group_input(db).flags(db).clone()); 58 | files_group_input(&new_db) 59 | .set_cfg_set(&mut new_db) 60 | .to(files_group_input(db).cfg_set(db).clone()); 61 | 62 | // Initiate it again instead of migrating because [`ExternalFiles.try_ext_as_virtual_obj`] is private. 63 | // We can do that since the only thing in this input is an `Arc` to a closure, 64 | // that is never supposed to be changed after the initialization. 65 | init_external_files(&mut new_db); 66 | 67 | new_db 68 | } 69 | 70 | fn new() -> Self { 71 | Self { 72 | storage: Default::default(), 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/lints/redundant_op.rs: -------------------------------------------------------------------------------- 1 | use super::{ADD, DIV, MUL, SUB}; 2 | use crate::context::{CairoLintKind, Lint}; 3 | 4 | use crate::helper::{is_one, is_zero}; 5 | use crate::lints::function_trait_name_from_fn_id; 6 | use crate::queries::{get_all_function_bodies, get_all_function_calls}; 7 | use cairo_lang_defs::ids::ModuleItemId; 8 | use cairo_lang_defs::plugin::PluginDiagnostic; 9 | use cairo_lang_diagnostics::Severity; 10 | use cairo_lang_semantic::{Arenas, ExprFunctionCall}; 11 | use cairo_lang_syntax::node::TypedStablePtr; 12 | use salsa::Database; 13 | 14 | pub struct RedundantOperation; 15 | 16 | /// ## What it does 17 | /// 18 | /// Checks for redundant arithmetic operations like `x + 0`, `x - 0`, `x * 1`, `x / 1` 19 | /// 20 | /// ## Example 21 | /// 22 | /// ```cairo 23 | /// fn main() { 24 | /// let x = 42; 25 | /// let _y = x * 1; 26 | /// } 27 | /// ``` 28 | /// 29 | /// Can be simplified to 30 | /// 31 | /// ```cairo 32 | /// fn main() { 33 | /// let x = 42; 34 | /// let _y = x; 35 | /// } 36 | /// ``` 37 | impl Lint for RedundantOperation { 38 | fn allowed_name(&self) -> &'static str { 39 | "redundant_op" 40 | } 41 | 42 | fn diagnostic_message(&self) -> &'static str { 43 | "This operation doesn't change the value and can be simplified." 44 | } 45 | 46 | fn kind(&self) -> CairoLintKind { 47 | CairoLintKind::RedundantOperation 48 | } 49 | } 50 | 51 | #[tracing::instrument(skip_all, level = "trace")] 52 | pub fn check_redundant_operation<'db>( 53 | db: &'db dyn Database, 54 | item: &ModuleItemId<'db>, 55 | diagnostics: &mut Vec>, 56 | ) { 57 | let function_bodies = get_all_function_bodies(db, item); 58 | for function_body in function_bodies.iter() { 59 | let function_call_exprs = get_all_function_calls(function_body); 60 | let arenas = &function_body.arenas; 61 | for function_call_expr in function_call_exprs { 62 | check_single_redundant_operation(db, &function_call_expr, arenas, diagnostics); 63 | } 64 | } 65 | } 66 | 67 | fn check_single_redundant_operation<'db>( 68 | db: &'db dyn Database, 69 | expr_func: &ExprFunctionCall<'db>, 70 | arenas: &Arenas<'db>, 71 | diagnostics: &mut Vec>, 72 | ) { 73 | let func = function_trait_name_from_fn_id(db, &expr_func.function); 74 | let is_redundant = match func.as_str() { 75 | ADD => is_zero(&expr_func.args[0], arenas) || is_zero(&expr_func.args[1], arenas), 76 | SUB => is_zero(&expr_func.args[1], arenas), 77 | MUL => is_one(&expr_func.args[0], arenas) || is_one(&expr_func.args[1], arenas), 78 | DIV => is_one(&expr_func.args[1], arenas), 79 | _ => false, 80 | }; 81 | 82 | if is_redundant { 83 | diagnostics.push(PluginDiagnostic { 84 | stable_ptr: expr_func.stable_ptr.untyped(), 85 | message: RedundantOperation.diagnostic_message().to_string(), 86 | severity: Severity::Warning, 87 | inner_span: None, 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/lints/erasing_op.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::ids::ModuleItemId; 2 | use cairo_lang_defs::plugin::PluginDiagnostic; 3 | use cairo_lang_diagnostics::Severity; 4 | use cairo_lang_semantic::{Arenas, ExprFunctionCall}; 5 | use cairo_lang_syntax::node::TypedStablePtr; 6 | 7 | use super::{AND, function_trait_name_from_fn_id}; 8 | use crate::context::{CairoLintKind, Lint}; 9 | 10 | use crate::helper::is_zero; 11 | use crate::lints::{DIV, MUL}; 12 | use crate::queries::{get_all_function_bodies, get_all_function_calls}; 13 | use salsa::Database; 14 | 15 | pub struct ErasingOperation; 16 | 17 | /// ## What it does 18 | /// 19 | /// Checks for operations that result in the value being erased (e.g., multiplication by 0 or 0 being divided by anything). 20 | /// 21 | /// ## Example 22 | /// 23 | /// ```cairo 24 | /// fn main() { 25 | /// let x = 1; 26 | /// let _y = 0 * x; 27 | /// let _z = 0 / x; 28 | /// let _c = x & 0; 29 | /// } 30 | /// ``` 31 | /// 32 | /// Could be simplified by replacing the entire expression with 0: 33 | /// 34 | /// ```cairo 35 | /// fn main() { 36 | /// let x = 1; 37 | /// let _y = 0; 38 | /// let _z = 0; 39 | /// let _c = 0; 40 | /// } 41 | /// ``` 42 | impl Lint for ErasingOperation { 43 | fn allowed_name(&self) -> &'static str { 44 | "erasing_op" 45 | } 46 | 47 | fn diagnostic_message(&self) -> &'static str { 48 | "This operation results in the value being erased (e.g., multiplication by 0). \ 49 | Consider replacing the entire expression with 0." 50 | } 51 | 52 | fn kind(&self) -> CairoLintKind { 53 | CairoLintKind::ErasingOperation 54 | } 55 | } 56 | 57 | #[tracing::instrument(skip_all, level = "trace")] 58 | pub fn check_erasing_operation<'db>( 59 | db: &'db dyn Database, 60 | item: &ModuleItemId<'db>, 61 | diagnostics: &mut Vec>, 62 | ) { 63 | let function_bodies = get_all_function_bodies(db, item); 64 | for function_body in function_bodies.iter() { 65 | let function_call_exprs = get_all_function_calls(function_body); 66 | let arenas = &function_body.arenas; 67 | for function_call_expr in function_call_exprs { 68 | check_single_erasing_operation(db, &function_call_expr, arenas, diagnostics); 69 | } 70 | } 71 | } 72 | 73 | fn check_single_erasing_operation<'db>( 74 | db: &'db dyn Database, 75 | expr_func: &ExprFunctionCall<'db>, 76 | arenas: &Arenas<'db>, 77 | diagnostics: &mut Vec>, 78 | ) { 79 | let func = function_trait_name_from_fn_id(db, &expr_func.function); 80 | 81 | let is_erasing_operation = match func.as_str() { 82 | MUL | AND => is_zero(&expr_func.args[0], arenas) || is_zero(&expr_func.args[1], arenas), 83 | DIV => is_zero(&expr_func.args[0], arenas), 84 | _ => false, 85 | }; 86 | if is_erasing_operation { 87 | diagnostics.push(PluginDiagnostic { 88 | stable_ptr: expr_func.stable_ptr.untyped(), 89 | message: ErasingOperation.diagnostic_message().to_string(), 90 | severity: Severity::Warning, 91 | inner_span: None, 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guideline 2 | 3 | 4 | ## Environment setup 5 | 6 | - Install the latest Rust via [rustup](https://doc.rust-lang.org/cargo/getting-started/installation.html). 7 | - Install the latest [Scarb via ASDF](https://docs.swmansion.com/scarb/download.html#install-via-asdf). 8 | 9 | ## Contributing 10 | 11 | - Before you open a pull request, it is always a good idea to search the issues and verify if the feature you would like 12 | to add hasn't been already discussed. 13 | - We also appreciate creating a feature request before making a contribution, so it can be discussed before you get to 14 | work. 15 | - If the change you are introducing is changing or breaking the behavior of any already existing features, make sure to 16 | include that information in the pull request description. 17 | 18 | ## Adding new lint rule 19 | 20 | In order to add a new rule, you must extend a [context](src/context.rs) with a new lint or whole lint group. 21 | 22 | Each individual lint rule should be documented. When implementing [Lint trait](src/context.rs#L118) for the Lint rule, remember to include a documentation for it which should look like this: 23 | 24 | ```rust 25 | /// ## What it does 26 | /// 27 | /// ## Example 28 | /// 29 | /// ```cairo 30 | /// // example code 31 | /// ``` 32 | impl Lint for MyRule { 33 | // implementation ... 34 | } 35 | ``` 36 | 37 | ## Updating documentation 38 | 39 | The documentation lives inside the `website` directory. The content is mainly autogenerated by 40 | ```bash 41 | cargo xtask update-docs 42 | ``` 43 | After implementing a new lint or after modifying old one's documentation, it is mandatory to update the documentation website by running the script above. 44 | 45 | ## Testing 46 | 47 | ### Running tests 48 | 49 | To run the tests, just use: 50 | 51 | ```sh 52 | cargo test 53 | ``` 54 | 55 | Remember to have `scarb` in your PATH, as it's used to resolve corelib used for testing. The Scarb version that should be used if testing is specified in [.tool-versions](.tool-versions) file. 56 | 57 | If you don't have an access to scarb binary, or you want to use specific version of the corelib during testing, just run: 58 | 59 | ```sh 60 | CORELIB_PATH="/path/to/corelib/src" cargo test 61 | ``` 62 | 63 | and use any corelib version you want. 64 | 65 | ### Reviewing snapshot changes 66 | 67 | ```sh 68 | cargo insta review 69 | ``` 70 | 71 | ### Manual instructions 72 | 73 | Each lint should have its own tests and should be extensive. To create a new test for a lint you need to create a new file/module 74 | in the [test_files folder](tests) and should be named as your lint. The file should 75 | 76 | As for tests, we are using [insta](https://insta.rs/) snapshot library. 77 | There are 2 testing macros: 78 | - [test_lint_diagnostics](tests/helpers/mod.rs) 79 | - [test_lint_fixer](tests/helpers/mod.rs) 80 | 81 | Tests should use only the inline snapshots. 82 | 83 | 84 | When creating a new test, you can run `CORELIB_PATH={path} cargo test`, and see if your snapshots match. It's recommended to use the the [cargo-insta](https://crates.io/crates/cargo-insta) tool to review the snapshots. Just remember to first run the tests with `cargo test`, and after that run `cargo insta review` to review any snapshot differences. 85 | -------------------------------------------------------------------------------- /src/lints/mod.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::ids::{FunctionWithBodyId, TopLevelLanguageElementId}; 2 | use cairo_lang_semantic::FunctionId; 3 | use cairo_lang_semantic::items::imp::ImplSemantic; 4 | use salsa::Database; 5 | 6 | pub mod bitwise_for_parity_check; 7 | pub mod bool_comparison; 8 | pub mod breaks; 9 | pub mod clone_on_copy; 10 | pub mod collapsible_match; 11 | pub mod double_comparison; 12 | pub mod double_parens; 13 | pub mod duplicate_underscore_args; 14 | pub mod empty_enum_brackets_variant; 15 | pub mod enum_variant_names; 16 | pub mod eq_op; 17 | pub mod erasing_op; 18 | pub mod ifs; 19 | pub mod int_op_one; 20 | pub mod loops; 21 | pub mod manual; 22 | pub mod panic; 23 | pub mod performance; 24 | pub mod redundant_brackets_in_enum_call; 25 | pub mod redundant_into; 26 | pub mod redundant_op; 27 | pub mod single_match; 28 | pub mod unit_return_type; 29 | pub mod unwrap_syscall; 30 | 31 | pub(crate) const LE: &str = "core::traits::PartialOrd::le"; 32 | pub(crate) const GE: &str = "core::traits::PartialOrd::ge"; 33 | pub(crate) const LT: &str = "core::traits::PartialOrd::lt"; 34 | pub(crate) const GT: &str = "core::traits::PartialOrd::gt"; 35 | pub(crate) const EQ: &str = "core::traits::PartialEq::eq"; 36 | pub(crate) const NE: &str = "core::traits::PartialEq::ne"; 37 | pub(crate) const AND: &str = "core::traits::BitAnd::bitand"; 38 | pub(crate) const OR: &str = "core::traits::BitOr::bitor"; 39 | pub(crate) const XOR: &str = "core::traits::BitXor::bitxor"; 40 | pub(crate) const NOT: &str = "core::traits::BitNot::bitnot"; 41 | pub(crate) const DIV: &str = "core::traits::Div::div"; 42 | pub(crate) const MUL: &str = "core::traits::Mul::mul"; 43 | pub(crate) const ADD: &str = "core::traits::Add::add"; 44 | pub(crate) const SUB: &str = "core::traits::Sub::sub"; 45 | pub(crate) const OK: &str = "core::result::Result::Ok"; 46 | pub(crate) const ERR: &str = "core::result::Result::Err"; 47 | pub(crate) const SOME: &str = "core::option::Option::Some"; 48 | pub(crate) const NONE: &str = "core::option::Option::None"; 49 | pub(crate) const TRUE: &str = "core::bool::True"; 50 | pub(crate) const FALSE: &str = "core::bool::False"; 51 | pub(crate) const PANIC_WITH_FELT252: &str = "core::panic_with_felt252"; 52 | pub(crate) const DEFAULT: &str = "core::traits::Default::default"; 53 | pub(crate) const ARRAY_NEW: &str = "core::array::ArrayTrait::new"; 54 | pub(crate) const NEVER: &str = "core::never"; 55 | pub(crate) const SPAN: &str = "core::array::Span"; 56 | pub(crate) const ARRAY: &str = "core::array::Array"; 57 | pub(crate) const U32: &str = "core::integer::u32"; 58 | 59 | pub(crate) fn function_trait_name_from_fn_id<'db>( 60 | db: &'db dyn Database, 61 | function: &FunctionId<'db>, 62 | ) -> String { 63 | let Ok(Some(func_id)) = function.get_concrete(db).body(db) else { 64 | return String::new(); 65 | }; 66 | // Get the trait function id of the function (if there's none it means it cannot be a call to 67 | // a corelib trait) 68 | let trait_fn_id = match func_id.function_with_body_id(db) { 69 | FunctionWithBodyId::Impl(func) => db.impl_function_trait_function(func).unwrap(), 70 | FunctionWithBodyId::Trait(func) => func, 71 | _ => return String::new(), 72 | }; 73 | // From the trait function id get the trait name and check if it's the corelib `BitAnd` 74 | trait_fn_id.full_path(db) 75 | } 76 | -------------------------------------------------------------------------------- /tests/helpers/scarb.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | path::{Path, PathBuf}, 4 | sync::OnceLock, 5 | }; 6 | 7 | use anyhow::{Context, Result}; 8 | use cairo_lang_filesystem::db::CORELIB_CRATE_NAME; 9 | use indoc::indoc; 10 | use scarb_metadata::{Metadata, MetadataCommand}; 11 | use tempfile::tempdir; 12 | use which::which; 13 | 14 | pub const SCARB_TOML: &str = "Scarb.toml"; 15 | 16 | fn get_scarb_path() -> Result { 17 | which("scarb").map_err(|_| anyhow::anyhow!("`scarb` not found in `PATH`")) 18 | } 19 | 20 | /// Calls `scarb metadata` on an empty Scarb package to find the `core` package. 21 | fn get_scarb_metadata(manifest: &Path) -> Result { 22 | let scarb_path = get_scarb_path()?; 23 | 24 | MetadataCommand::new() 25 | .scarb_path(scarb_path) 26 | .manifest_path(manifest) 27 | .inherit_stderr() 28 | .exec() 29 | .context("failed to execute: scarb metadata") 30 | } 31 | 32 | /// Try to find a Scarb-managed `core` package if we have Scarb toolchain. 33 | /// 34 | /// The easiest way to do this is to create an empty Scarb package and run `scarb metadata` on it. 35 | /// The `core` package will be a component of this empty package. 36 | /// For minimal packages, `scarb metadata` should be pretty fast. 37 | pub fn find_scarb_managed_core() -> Option { 38 | let lookup = || { 39 | let workspace = tempdir() 40 | .context("failed to create temporary directory") 41 | .inspect_err(|e| eprintln!("{e:?}")) 42 | .ok()?; 43 | 44 | let scarb_toml = workspace.path().join(SCARB_TOML); 45 | fs::write( 46 | &scarb_toml, 47 | indoc! {r#" 48 | [package] 49 | name = "cairo_lint_unmanaged_core_lookup" 50 | version = "1.0.0" 51 | "#}, 52 | ) 53 | .context("failed to write Scarb.toml") 54 | .inspect_err(|e| eprintln!("{e:?}")) 55 | .ok()?; 56 | 57 | let metadata = get_scarb_metadata(&scarb_toml) 58 | .inspect_err(|e| eprintln!("{e:?}")) 59 | .ok()?; 60 | 61 | // Ensure the workspace directory is deleted after running Scarb. 62 | // We are ignoring the error, leaving doing proper clean-up to the OS. 63 | let _ = workspace 64 | .close() 65 | .context("failed to wipe temporary directory") 66 | .inspect_err(|e| eprintln!("{e:?}")); 67 | 68 | // Scarb is expected to generate only one compilation unit (for our stub package) 69 | // that will consist of this package and the `core` crate. 70 | // Therefore, we allow ourselves to liberally just look for any first usage of a package 71 | // named `core` in all compilation units components we got. 72 | let path = metadata 73 | .compilation_units 74 | .into_iter() 75 | .find_map(|compilation_unit| { 76 | compilation_unit 77 | .components 78 | .iter() 79 | .find(|component| component.name == CORELIB_CRATE_NAME) 80 | .map(|component| component.source_root().to_path_buf().into_std_path_buf()) 81 | })?; 82 | 83 | Some(path) 84 | }; 85 | 86 | static CACHE: OnceLock> = OnceLock::new(); 87 | CACHE.get_or_init(lookup).clone() 88 | } 89 | -------------------------------------------------------------------------------- /tests/breaks/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{test_lint_diagnostics, test_lint_fixer}; 2 | 3 | const SIMPLE_BREAK: &str = r#" 4 | fn main() { 5 | loop { 6 | break (); 7 | } 8 | } 9 | "#; 10 | 11 | const SIMPLE_BREAK_ALLOWED: &str = r#" 12 | fn main() { 13 | loop { 14 | #[allow(break_unit)] 15 | break (); 16 | } 17 | } 18 | "#; 19 | 20 | const BREAK_INSIDE_OF_IF: &str = r#" 21 | fn main() { 22 | let mut a = 1_u32; 23 | #[allow(loop_for_while)] 24 | loop { 25 | if a == 10 { 26 | break (); 27 | } 28 | a += 1; 29 | } 30 | } 31 | "#; 32 | 33 | const BREAK_INSIDE_OF_IF_WITH_COMMENT: &str = r#" 34 | fn main() { 35 | let mut a = 1_u32; 36 | #[allow(loop_for_while)] 37 | loop { 38 | if a == 10 { 39 | // this is a break 40 | break (); 41 | // this was a break 42 | } 43 | a += 1; 44 | } 45 | } 46 | "#; 47 | 48 | #[test] 49 | fn simple_break_diagnostics() { 50 | test_lint_diagnostics!(SIMPLE_BREAK, @r" 51 | Plugin diagnostic: unnecessary double parentheses found after break. Consider removing them. 52 | --> lib.cairo:4:8 53 | break (); 54 | ^^^^^^^^^ 55 | "); 56 | } 57 | 58 | #[test] 59 | fn simple_break_fixer() { 60 | test_lint_fixer!(SIMPLE_BREAK, @r" 61 | fn main() { 62 | loop { 63 | break; 64 | } 65 | } 66 | "); 67 | } 68 | 69 | #[test] 70 | fn simple_break_allowed_diagnostics() { 71 | test_lint_diagnostics!(SIMPLE_BREAK_ALLOWED, @""); 72 | } 73 | 74 | #[test] 75 | fn simple_break_allowed_fixer() { 76 | test_lint_fixer!(SIMPLE_BREAK_ALLOWED, @r" 77 | fn main() { 78 | loop { 79 | #[allow(break_unit)] 80 | break (); 81 | } 82 | } 83 | "); 84 | } 85 | 86 | #[test] 87 | fn break_inside_of_if_diagnostics() { 88 | test_lint_diagnostics!(BREAK_INSIDE_OF_IF, @r" 89 | Plugin diagnostic: unnecessary double parentheses found after break. Consider removing them. 90 | --> lib.cairo:7:13 91 | break (); 92 | ^^^^^^^^^ 93 | "); 94 | } 95 | 96 | #[test] 97 | fn break_inside_of_if_fixer() { 98 | test_lint_fixer!(BREAK_INSIDE_OF_IF, @r#" 99 | fn main() { 100 | let mut a = 1_u32; 101 | #[allow(loop_for_while)] 102 | loop { 103 | if a == 10 { 104 | break; 105 | } 106 | a += 1; 107 | } 108 | } 109 | "#); 110 | } 111 | 112 | #[test] 113 | fn break_inside_of_if_with_comment_diagnostics() { 114 | test_lint_diagnostics!(BREAK_INSIDE_OF_IF_WITH_COMMENT, @r" 115 | Plugin diagnostic: unnecessary double parentheses found after break. Consider removing them. 116 | --> lib.cairo:8:13 117 | break (); 118 | ^^^^^^^^^ 119 | "); 120 | } 121 | 122 | #[test] 123 | fn break_inside_of_if_with_comment_fixer() { 124 | test_lint_fixer!(BREAK_INSIDE_OF_IF_WITH_COMMENT, @r#" 125 | fn main() { 126 | let mut a = 1_u32; 127 | #[allow(loop_for_while)] 128 | loop { 129 | if a == 10 { 130 | // this is a break 131 | break; 132 | // this was a break 133 | } 134 | a += 1; 135 | } 136 | } 137 | "#); 138 | } 139 | -------------------------------------------------------------------------------- /src/lints/bitwise_for_parity_check.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::ids::{FunctionWithBodyId, ModuleItemId, TopLevelLanguageElementId}; 2 | use cairo_lang_defs::plugin::PluginDiagnostic; 3 | use cairo_lang_diagnostics::Severity; 4 | use cairo_lang_semantic::items::imp::ImplSemantic; 5 | use cairo_lang_semantic::{Arenas, Expr, ExprFunctionCall, ExprFunctionCallArg}; 6 | use cairo_lang_syntax::node::TypedStablePtr; 7 | use if_chain::if_chain; 8 | use num_bigint::BigInt; 9 | 10 | use crate::context::{CairoLintKind, Lint}; 11 | 12 | use crate::queries::{get_all_function_bodies, get_all_function_calls}; 13 | 14 | use super::AND; 15 | use salsa::Database; 16 | 17 | pub struct BitwiseForParity; 18 | 19 | /// ## What it does 20 | /// 21 | /// Checks for `x & 1` which is unoptimized in cairo and could be replaced by `x % 1`. 22 | /// 23 | /// ## Example 24 | /// 25 | /// ```cairo 26 | /// fn main() { 27 | /// let _a = 200_u32 & 1; 28 | /// } 29 | /// ``` 30 | impl Lint for BitwiseForParity { 31 | fn allowed_name(&self) -> &'static str { 32 | "bitwise_for_parity_check" 33 | } 34 | 35 | fn diagnostic_message(&self) -> &'static str { 36 | "You seem to be trying to use `&` for parity check. Consider using `DivRem::div_rem()` instead." 37 | } 38 | 39 | fn kind(&self) -> CairoLintKind { 40 | CairoLintKind::BitwiseForParityCheck 41 | } 42 | } 43 | 44 | /// Checks for `x & 1` which is unoptimized in cairo and can be replaced by `x % 1` 45 | #[tracing::instrument(skip_all, level = "trace")] 46 | pub fn check_bitwise_for_parity<'db>( 47 | db: &'db dyn Database, 48 | item: &ModuleItemId<'db>, 49 | diagnostics: &mut Vec>, 50 | ) { 51 | let function_bodies = get_all_function_bodies(db, item); 52 | for function_body in function_bodies.iter() { 53 | let function_call_exprs = get_all_function_calls(function_body); 54 | let arenas = &function_body.arenas; 55 | for function_call_expr in function_call_exprs { 56 | check_single_bitwise_for_parity(db, &function_call_expr, arenas, diagnostics); 57 | } 58 | } 59 | } 60 | 61 | fn check_single_bitwise_for_parity<'db>( 62 | db: &'db dyn Database, 63 | function_call_expr: &ExprFunctionCall<'db>, 64 | arenas: &Arenas<'db>, 65 | diagnostics: &mut Vec>, 66 | ) { 67 | let Ok(Some(func_id)) = function_call_expr.function.get_concrete(db).body(db) else { 68 | return; 69 | }; 70 | // Get the trait function id of the function (if there's none it means it cannot be a call to 71 | // `bitand`) 72 | let trait_fn_id = match func_id.function_with_body_id(db) { 73 | FunctionWithBodyId::Impl(func) => db.impl_function_trait_function(func).unwrap(), 74 | FunctionWithBodyId::Trait(func) => func, 75 | _ => return, 76 | }; 77 | 78 | // From the trait function id get the trait name and check if it's the corelib `BitAnd` 79 | if_chain! { 80 | if trait_fn_id.full_path(db) == AND; 81 | if let ExprFunctionCallArg::Value(val) = function_call_expr.args[1]; 82 | // Checks if the rhs is 1 83 | if let Expr::Literal(lit) = &arenas.exprs[val]; 84 | if lit.value == BigInt::from(1u8); 85 | then { 86 | diagnostics.push(PluginDiagnostic { 87 | stable_ptr: function_call_expr.stable_ptr.untyped(), 88 | message: BitwiseForParity.diagnostic_message().to_string(), 89 | severity: Severity::Warning, 90 | inner_span: None, 91 | }); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/lints/breaks.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::ids::ModuleItemId; 2 | use cairo_lang_defs::plugin::PluginDiagnostic; 3 | use cairo_lang_diagnostics::Severity; 4 | use cairo_lang_semantic::{Arenas, StatementBreak}; 5 | 6 | use cairo_lang_syntax::node::{SyntaxNode, TypedStablePtr}; 7 | use if_chain::if_chain; 8 | 9 | use crate::context::{CairoLintKind, Lint}; 10 | 11 | use crate::fixer::InternalFix; 12 | use crate::queries::{get_all_break_statements, get_all_function_bodies}; 13 | use salsa::Database; 14 | 15 | pub struct BreakUnit; 16 | 17 | /// ## What it does 18 | /// 19 | /// Checks for `break ();` statements and suggests removing the parentheses. 20 | /// 21 | /// ## Example 22 | /// 23 | /// ```cairo 24 | /// fn main() { 25 | /// loop { 26 | /// break (); 27 | /// } 28 | /// } 29 | /// ``` 30 | /// 31 | /// Can be fixed by removing the parentheses: 32 | /// 33 | /// ```cairo 34 | /// fn main() { 35 | /// loop { 36 | /// break; 37 | /// } 38 | /// } 39 | /// ``` 40 | impl Lint for BreakUnit { 41 | fn allowed_name(&self) -> &'static str { 42 | "break_unit" 43 | } 44 | 45 | fn diagnostic_message(&self) -> &'static str { 46 | "unnecessary double parentheses found after break. Consider removing them." 47 | } 48 | 49 | fn kind(&self) -> CairoLintKind { 50 | CairoLintKind::BreakUnit 51 | } 52 | 53 | fn has_fixer(&self) -> bool { 54 | true 55 | } 56 | 57 | fn fix<'db>(&self, db: &'db dyn Database, node: SyntaxNode<'db>) -> Option> { 58 | fix_break_unit(db, node) 59 | } 60 | 61 | fn fix_message(&self) -> Option<&'static str> { 62 | Some("Remove unnecessary parentheses from break") 63 | } 64 | } 65 | 66 | #[tracing::instrument(skip_all, level = "trace")] 67 | pub fn check_break<'db>( 68 | db: &'db dyn Database, 69 | item: &ModuleItemId<'db>, 70 | diagnostics: &mut Vec>, 71 | ) { 72 | let function_bodies = get_all_function_bodies(db, item); 73 | for function_body in function_bodies { 74 | let break_exprs = get_all_break_statements(function_body); 75 | for break_expr in break_exprs.iter() { 76 | check_single_break(db, break_expr, &function_body.arenas, diagnostics) 77 | } 78 | } 79 | } 80 | 81 | fn check_single_break<'db>( 82 | db: &'db dyn Database, 83 | break_expr: &StatementBreak<'db>, 84 | arenas: &Arenas<'db>, 85 | diagnostics: &mut Vec>, 86 | ) { 87 | if_chain! { 88 | if let Some(expr) = break_expr.expr_option; 89 | if arenas.exprs[expr].ty().is_unit(db); 90 | then { 91 | diagnostics.push(PluginDiagnostic { 92 | stable_ptr: break_expr.stable_ptr.untyped(), 93 | message: BreakUnit.diagnostic_message().to_string(), 94 | severity: Severity::Warning, 95 | inner_span: None 96 | }); 97 | } 98 | } 99 | } 100 | 101 | /// Rewrites `break ();` as `break;` given the node text contains it. 102 | #[tracing::instrument(skip_all, level = "trace")] 103 | pub fn fix_break_unit<'db>( 104 | db: &'db dyn Database, 105 | node: SyntaxNode<'db>, 106 | ) -> Option> { 107 | Some(InternalFix { 108 | node, 109 | suggestion: node.get_text(db).replace("break ();", "break;").to_string(), 110 | description: BreakUnit.fix_message().unwrap().to_string(), 111 | import_addition_paths: None, 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /website/.vitepress/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 68 | -------------------------------------------------------------------------------- /website/.vitepress/theme/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Customize default theme styling by overriding CSS variables: 3 | * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css 4 | */ 5 | 6 | :root { 7 | --swm-c-blue-100: #38acdd; 8 | --swm-c-blue-120: #126893; 9 | --swm-c-blue-140: #1b4865; 10 | --swm-c-blue-20: #e1f3fa; 11 | --swm-c-blue-40: #b5e1f1; 12 | --swm-c-blue-60: #87cce8; 13 | --swm-c-blue-80: #5bb9e0; 14 | --swm-c-green-100: #57b495; 15 | --swm-c-green-120: #31775d; 16 | --swm-c-green-140: #2a4f4a; 17 | --swm-c-green-20: #ebfcf7; 18 | --swm-c-green-40: #dff2ec; 19 | --swm-c-green-60: #b1dfd0; 20 | --swm-c-green-80: #82cab2; 21 | --swm-c-navy-100: #001a72; 22 | --swm-c-navy-20: #c1c6e5; 23 | --swm-c-navy-40: #919fcf; 24 | --swm-c-navy-60: #6676aa; 25 | --swm-c-navy-80: #33488e; 26 | --swm-c-purple-100: #782aeb; 27 | --swm-c-purple-120: #6a539a; 28 | --swm-c-purple-140: #473d68; 29 | --swm-c-purple-20: #f5eeff; 30 | --swm-c-purple-40: #e8dafc; 31 | --swm-c-purple-60: #d1bbf3; 32 | --swm-c-purple-80: #b58df1; 33 | --swm-c-red-100: #ff6259; 34 | --swm-c-red-120: #914f55; 35 | --swm-c-red-140: #5a3b46; 36 | --swm-c-red-20: #ffedf0; 37 | --swm-c-red-40: #ffd2d7; 38 | --swm-c-red-60: #ffa3a1; 39 | --swm-c-red-80: #fa7f7c; 40 | --swm-c-yellow-100: #ffd61e; 41 | --swm-c-yellow-120: #91823d; 42 | --swm-c-yellow-140: #5a553a; 43 | --swm-c-yellow-20: #fffae1; 44 | --swm-c-yellow-40: #fff1b2; 45 | --swm-c-yellow-60: #ffe780; 46 | --swm-c-yellow-80: #ffe04b; 47 | } 48 | 49 | .dark { 50 | --swm-c-blue-100: #00a9f0; 51 | --swm-c-blue-40: #d7f0fa; 52 | --swm-c-blue-60: #a8dbf0; 53 | --swm-c-blue-80: #6fcef5; 54 | --swm-c-green-100: #3fc684; 55 | --swm-c-green-40: #d3f5e4; 56 | --swm-c-green-60: #a0dfc0; 57 | --swm-c-green-80: #7adead; 58 | --swm-c-navy-100: #001a72; 59 | --swm-c-navy-120: #122154; 60 | --swm-c-navy-140: #1b2445; 61 | --swm-c-navy-40: #abbcf5; 62 | --swm-c-navy-60: #7485bd; 63 | --swm-c-navy-80: #0a2688; 64 | --swm-c-purple-100: #b07eff; 65 | --swm-c-purple-40: #e9dbff; 66 | --swm-c-purple-60: #d0b2ff; 67 | --swm-c-purple-80: #c49ffe; 68 | --swm-c-red-100: #ff7774; 69 | --swm-c-red-40: #ffdcdb; 70 | --swm-c-red-60: #ffb4b2; 71 | --swm-c-red-80: #ff8b88; 72 | --swm-c-yellow-100: #fd4; 73 | --swm-c-yellow-40: #fff9db; 74 | --swm-c-yellow-60: #fff1b2; 75 | --swm-c-yellow-80: #ffe678; 76 | } 77 | 78 | :root { 79 | --swm-font-family-geometric-humanist: Avenir, Montserrat, Corbel, "URW Gothic", source-sans-pro, 80 | sans-serif; 81 | } 82 | 83 | :root { 84 | --vp-c-brand-1: var(--swm-c-navy-80); 85 | --vp-c-brand-2: var(--swm-c-navy-60); 86 | --vp-c-brand-3: var(--swm-c-navy-100); 87 | --vp-c-brand-soft: var(--swm-c-navy-20); 88 | 89 | --vp-c-tip-1: var(--swm-c-blue-120); 90 | --vp-c-tip-2: var(--swm-c-blue-140); 91 | --vp-c-tip-3: black; 92 | --vp-c-tip-soft: var(--swm-c-blue-20); 93 | --vp-custom-block-tip-code-bg: var(--swm-c-blue-40); 94 | } 95 | 96 | .dark { 97 | --vp-c-brand-1: var(--swm-c-navy-60); 98 | --vp-c-brand-2: var(--swm-c-navy-40); 99 | --vp-c-brand-3: var(--swm-c-navy-80); 100 | --vp-c-brand-soft: var(--swm-c-navy-120); 101 | 102 | --vp-c-tip-1: var(--swm-c-blue-40); 103 | --vp-c-tip-2: var(--swm-c-blue-60); 104 | --vp-c-tip-3: var(--swm-c-blue-80); 105 | --vp-c-tip-soft: var(--swm-c-blue-140); 106 | --vp-custom-block-tip-code-bg: var(--swm-c-blue-120); 107 | } 108 | 109 | :root { 110 | --vp-nav-logo-height: 40px; 111 | } 112 | -------------------------------------------------------------------------------- /tests/empty_enum_brackets_variant/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{test_lint_diagnostics, test_lint_fixer}; 2 | 3 | const MULTIPLE_EMPTY_VARIANTS: &str = r#" 4 | #[derive(Drop)] 5 | enum MyEnum { 6 | Data: u8, 7 | Empty1: (), // Some comment 8 | Empty2: ( ), // Different comment 9 | Empty3 10 | } 11 | "#; 12 | 13 | const CORRECT_VARIANT: &str = r#" 14 | #[derive(Drop)] 15 | enum MyEnum { 16 | Data: u8, 17 | Empty, 18 | } 19 | "#; 20 | 21 | const LAST_EMPTY_VARIANT_WITH_COMMENT: &str = r#" 22 | #[derive(Drop)] 23 | enum MyEnum { 24 | Data: u8, 25 | Empty: () // Comment 26 | } 27 | "#; 28 | 29 | const ALLOW_MULTIPLE_EMPTY_VARIANTS: &str = r#" 30 | #[derive(Drop)] 31 | #[allow(empty_enum_brackets_variant)] 32 | enum MyEnum { 33 | Data: u8, 34 | Empty1: (), 35 | Empty2: (), 36 | Empty3 37 | } 38 | "#; 39 | 40 | const TUPLE_VARIANT: &str = r#" 41 | #[derive(Drop)] 42 | enum MyEnum { 43 | Data: u8, 44 | Tuple: (u8, u8), 45 | } 46 | "#; 47 | 48 | const USER_DEFINED_UNIT_VARIANT: &str = r#" 49 | type Unit = (); 50 | #[derive(Drop)] 51 | enum MyEnum { 52 | Data: u8, 53 | Empty: Unit, 54 | } 55 | "#; 56 | 57 | #[test] 58 | fn multiple_empty_variants_diagnostics() { 59 | test_lint_diagnostics!(MULTIPLE_EMPTY_VARIANTS, @r" 60 | Plugin diagnostic: redundant parentheses in enum variant definition 61 | --> lib.cairo:5:5 62 | Empty1: (), // Some comment 63 | ^^^^^^^^^^ 64 | Plugin diagnostic: redundant parentheses in enum variant definition 65 | --> lib.cairo:6:5 66 | Empty2: ( ), // Different comment 67 | ^^^^^^^^^^^^^^^^^^ 68 | "); 69 | } 70 | 71 | #[test] 72 | fn correct_variant_diagnostics() { 73 | test_lint_diagnostics!(CORRECT_VARIANT, @""); 74 | } 75 | 76 | #[test] 77 | fn last_empty_variant_with_comment_diagnostics() { 78 | test_lint_diagnostics!(LAST_EMPTY_VARIANT_WITH_COMMENT, @r" 79 | Plugin diagnostic: redundant parentheses in enum variant definition 80 | --> lib.cairo:5:5 81 | Empty: () // Comment 82 | ^^^^^^^^^ 83 | "); 84 | } 85 | 86 | #[test] 87 | fn allow_multiple_empty_variants_diagnostics() { 88 | test_lint_diagnostics!(ALLOW_MULTIPLE_EMPTY_VARIANTS, @""); 89 | } 90 | 91 | #[test] 92 | fn tuple_variant_diagnostics() { 93 | test_lint_diagnostics!(TUPLE_VARIANT, @""); 94 | } 95 | 96 | #[test] 97 | fn user_defined_unit_variant_diagnostics() { 98 | test_lint_diagnostics!(USER_DEFINED_UNIT_VARIANT, @r" 99 | Plugin diagnostic: redundant parentheses in enum variant definition 100 | --> lib.cairo:6:5 101 | Empty: Unit, 102 | ^^^^^^^^^^^ 103 | "); 104 | } 105 | 106 | #[test] 107 | fn multiple_empty_variants_fixer() { 108 | test_lint_fixer!(MULTIPLE_EMPTY_VARIANTS, @r" 109 | #[derive(Drop)] 110 | enum MyEnum { 111 | Data: u8, 112 | Empty1, // Some comment 113 | Empty2, // Different comment 114 | Empty3, 115 | } 116 | "); 117 | } 118 | 119 | #[test] 120 | fn last_empty_variant_with_comment_fixer() { 121 | test_lint_fixer!(LAST_EMPTY_VARIANT_WITH_COMMENT, @r" 122 | #[derive(Drop)] 123 | enum MyEnum { 124 | Data: u8, 125 | Empty // Comment 126 | } 127 | "); 128 | } 129 | 130 | #[test] 131 | fn user_defined_unit_variant_fixer() { 132 | test_lint_fixer!(USER_DEFINED_UNIT_VARIANT, @r" 133 | type Unit = (); 134 | #[derive(Drop)] 135 | enum MyEnum { 136 | Data: u8, 137 | Empty, 138 | } 139 | "); 140 | } 141 | 142 | #[test] 143 | fn allow_multiple_empty_variants_fixer() { 144 | test_lint_fixer!(ALLOW_MULTIPLE_EMPTY_VARIANTS, @r" 145 | #[derive(Drop)] 146 | #[allow(empty_enum_brackets_variant)] 147 | enum MyEnum { 148 | Data: u8, 149 | Empty1: (), 150 | Empty2: (), 151 | Empty3, 152 | } 153 | "); 154 | } 155 | -------------------------------------------------------------------------------- /src/lints/empty_enum_brackets_variant.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::{ids::ModuleItemId, plugin::PluginDiagnostic}; 2 | use cairo_lang_diagnostics::Severity; 3 | use cairo_lang_semantic::items::enm::EnumSemantic; 4 | use cairo_lang_syntax::node::{ 5 | SyntaxNode, TypedStablePtr, TypedSyntaxNode, 6 | ast::{self, OptionTypeClause}, 7 | }; 8 | 9 | use crate::{ 10 | context::{CairoLintKind, Lint}, 11 | fixer::InternalFix, 12 | }; 13 | use salsa::Database; 14 | 15 | pub struct EmptyEnumBracketsVariant; 16 | 17 | /// ## What it does 18 | /// 19 | /// Finds enum variants that are declared with empty brackets. 20 | /// 21 | /// ## Example 22 | /// 23 | /// ```cairo 24 | /// enum MyEnum { 25 | /// Data: u8, 26 | /// Empty: () // redundant parentheses 27 | /// } 28 | /// ``` 29 | /// 30 | /// Can be simplified to: 31 | /// 32 | /// ```cairo 33 | /// enum MyEnum { 34 | /// Data(u8), 35 | /// Empty, 36 | /// } 37 | /// ``` 38 | impl Lint for EmptyEnumBracketsVariant { 39 | fn allowed_name(&self) -> &'static str { 40 | "empty_enum_brackets_variant" 41 | } 42 | 43 | fn diagnostic_message(&self) -> &'static str { 44 | "redundant parentheses in enum variant definition" 45 | } 46 | 47 | fn kind(&self) -> CairoLintKind { 48 | CairoLintKind::EnumEmptyVariantBrackets 49 | } 50 | 51 | fn has_fixer(&self) -> bool { 52 | true 53 | } 54 | 55 | fn fix<'db>(&self, db: &'db dyn Database, node: SyntaxNode<'db>) -> Option> { 56 | fix_empty_enum_brackets_variant(db, node) 57 | } 58 | 59 | fn fix_message(&self) -> Option<&'static str> { 60 | Some("Remove unit type definition from enum variant") 61 | } 62 | } 63 | 64 | #[tracing::instrument(skip_all, level = "trace")] 65 | pub fn check_empty_enum_brackets_variant<'db>( 66 | db: &'db dyn Database, 67 | item: &ModuleItemId<'db>, 68 | diagnostics: &mut Vec>, 69 | ) { 70 | let ModuleItemId::Enum(enum_id) = item else { 71 | return; 72 | }; 73 | 74 | let Ok(variants) = db.enum_variants(*enum_id) else { 75 | return; 76 | }; 77 | 78 | for variant in variants.values() { 79 | let Ok(semantic_variant) = db.variant_semantic(*enum_id, *variant) else { 80 | return; 81 | }; 82 | 83 | // Check if the variant is of unit type `()` 84 | if semantic_variant.ty.is_unit(db) { 85 | let ast_variant = variant.stable_ptr(db).lookup(db); 86 | 87 | // Determine if the variant includes a type clause, or if the type clause is empty 88 | if let OptionTypeClause::TypeClause(_) = ast_variant.type_clause(db) { 89 | diagnostics.push(PluginDiagnostic { 90 | stable_ptr: variant.stable_ptr(db).untyped(), 91 | message: EmptyEnumBracketsVariant.diagnostic_message().to_string(), 92 | severity: Severity::Warning, 93 | inner_span: None, 94 | }); 95 | } 96 | } 97 | } 98 | } 99 | 100 | #[tracing::instrument(skip_all, level = "trace")] 101 | fn fix_empty_enum_brackets_variant<'db>( 102 | db: &'db dyn Database, 103 | node: SyntaxNode<'db>, 104 | ) -> Option> { 105 | let ast_variant = ast::Variant::from_syntax_node(db, node); 106 | 107 | // Extract a clean type definition, to remove 108 | let type_clause = ast_variant 109 | .type_clause(db) 110 | .as_syntax_node() 111 | .get_text_without_trivia(db); 112 | 113 | let variant_text = node.get_text(db); 114 | let fixed = variant_text.replace(type_clause.long(db).as_str(), ""); 115 | 116 | Some(InternalFix { 117 | node, 118 | suggestion: fixed, 119 | description: EmptyEnumBracketsVariant.fix_message().unwrap().to_string(), 120 | import_addition_paths: None, 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /src/lints/manual/manual_err.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::ids::ModuleItemId; 2 | use cairo_lang_defs::plugin::PluginDiagnostic; 3 | use cairo_lang_diagnostics::Severity; 4 | 5 | use cairo_lang_syntax::node::{SyntaxNode, TypedStablePtr}; 6 | 7 | use crate::context::{CairoLintKind, Lint}; 8 | 9 | use crate::fixer::InternalFix; 10 | use crate::lints::manual::{ManualLint, check_manual, check_manual_if}; 11 | use crate::queries::{get_all_function_bodies, get_all_if_expressions, get_all_match_expressions}; 12 | 13 | use super::helpers::fix_manual; 14 | 15 | use salsa::Database; 16 | 17 | pub struct ManualErr; 18 | 19 | /// ## What it does 20 | /// 21 | /// Checks for manual implementations of `err` in match and if expressions. 22 | /// 23 | /// ## Example 24 | /// 25 | /// ```cairo 26 | /// fn main() { 27 | /// let foo: Result = Result::Err('err'); 28 | /// let _foo = match foo { 29 | /// Result::Ok(_) => Option::None, 30 | /// Result::Err(x) => Option::Some(x), 31 | /// }; 32 | /// } 33 | /// ``` 34 | /// 35 | /// Can be rewritten as: 36 | /// 37 | /// ```cairo 38 | /// fn main() { 39 | /// let foo: Result = Result::Err('err'); 40 | /// let _foo = foo.err(); 41 | /// } 42 | /// ``` 43 | impl Lint for ManualErr { 44 | fn allowed_name(&self) -> &'static str { 45 | "manual_err" 46 | } 47 | 48 | fn diagnostic_message(&self) -> &'static str { 49 | "Manual match for `err` detected. Consider using `err()` instead" 50 | } 51 | 52 | fn kind(&self) -> CairoLintKind { 53 | CairoLintKind::ManualErr 54 | } 55 | 56 | fn has_fixer(&self) -> bool { 57 | true 58 | } 59 | 60 | fn fix<'db>(&self, db: &'db dyn Database, node: SyntaxNode<'db>) -> Option> { 61 | fix_manual_err(db, node) 62 | } 63 | 64 | fn fix_message(&self) -> Option<&'static str> { 65 | Some("Replace manual `err` with `err()` method") 66 | } 67 | } 68 | 69 | #[tracing::instrument(skip_all, level = "trace")] 70 | pub fn check_manual_err<'db>( 71 | db: &'db dyn Database, 72 | item: &ModuleItemId<'db>, 73 | diagnostics: &mut Vec>, 74 | ) { 75 | let function_bodies = get_all_function_bodies(db, item); 76 | for function_body in function_bodies.iter() { 77 | let match_exprs = get_all_match_expressions(function_body); 78 | let if_exprs = get_all_if_expressions(function_body); 79 | let arenas = &function_body.arenas; 80 | for match_expr in match_exprs.iter() { 81 | if check_manual(db, match_expr, arenas, ManualLint::ManualErr) { 82 | diagnostics.push(PluginDiagnostic { 83 | stable_ptr: match_expr.stable_ptr.untyped(), 84 | message: ManualErr.diagnostic_message().to_owned(), 85 | severity: Severity::Warning, 86 | inner_span: None, 87 | }); 88 | } 89 | } 90 | for if_expr in if_exprs.iter() { 91 | if check_manual_if(db, if_expr, arenas, ManualLint::ManualErr) { 92 | diagnostics.push(PluginDiagnostic { 93 | stable_ptr: if_expr.stable_ptr.untyped(), 94 | message: ManualErr.diagnostic_message().to_owned(), 95 | severity: Severity::Warning, 96 | inner_span: None, 97 | }); 98 | } 99 | } 100 | } 101 | } 102 | 103 | /// Rewrites a manual implementation of err 104 | #[tracing::instrument(skip_all, level = "trace")] 105 | pub fn fix_manual_err<'db>( 106 | db: &'db dyn Database, 107 | node: SyntaxNode<'db>, 108 | ) -> Option> { 109 | Some(InternalFix { 110 | node, 111 | suggestion: fix_manual("err", db, node), 112 | description: ManualErr.fix_message().unwrap().to_string(), 113 | import_addition_paths: None, 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /src/lints/manual/manual_ok.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::ids::ModuleItemId; 2 | use cairo_lang_defs::plugin::PluginDiagnostic; 3 | use cairo_lang_diagnostics::Severity; 4 | 5 | use cairo_lang_syntax::node::{SyntaxNode, TypedStablePtr}; 6 | 7 | use crate::context::{CairoLintKind, Lint}; 8 | 9 | use crate::fixer::InternalFix; 10 | use crate::lints::manual::{ManualLint, check_manual, check_manual_if}; 11 | use crate::queries::{get_all_function_bodies, get_all_if_expressions, get_all_match_expressions}; 12 | 13 | use super::helpers::fix_manual; 14 | 15 | use salsa::Database; 16 | 17 | pub struct ManualOk; 18 | 19 | /// ## What it does 20 | /// 21 | /// Checks for manual implementation of `ok` method in match and if expressions. 22 | /// 23 | /// ## Example 24 | /// 25 | /// ```cairo 26 | /// fn main() { 27 | /// let res_val: Result = Result::Err('err'); 28 | /// let _a = match res_val { 29 | /// Result::Ok(x) => Option::Some(x), 30 | /// Result::Err(_) => Option::None, 31 | /// }; 32 | /// } 33 | /// ``` 34 | /// 35 | /// Can be replaced with: 36 | /// 37 | /// ```cairo 38 | /// fn main() { 39 | /// let res_val: Result = Result::Err('err'); 40 | /// let _a = res_val.ok(); 41 | /// } 42 | /// ``` 43 | impl Lint for ManualOk { 44 | fn allowed_name(&self) -> &'static str { 45 | "manual_ok" 46 | } 47 | 48 | fn diagnostic_message(&self) -> &'static str { 49 | "Manual match for `ok` detected. Consider using `ok()` instead" 50 | } 51 | 52 | fn kind(&self) -> CairoLintKind { 53 | CairoLintKind::ManualOk 54 | } 55 | 56 | fn has_fixer(&self) -> bool { 57 | true 58 | } 59 | 60 | fn fix<'db>(&self, db: &'db dyn Database, node: SyntaxNode<'db>) -> Option> { 61 | fix_manual_ok(db, node) 62 | } 63 | 64 | fn fix_message(&self) -> Option<&'static str> { 65 | Some("Replace manual conversion with `ok()` method") 66 | } 67 | } 68 | 69 | #[tracing::instrument(skip_all, level = "trace")] 70 | pub fn check_manual_ok<'db>( 71 | db: &'db dyn Database, 72 | item: &ModuleItemId<'db>, 73 | diagnostics: &mut Vec>, 74 | ) { 75 | let function_bodies = get_all_function_bodies(db, item); 76 | for function_body in function_bodies.iter() { 77 | let if_exprs = get_all_if_expressions(function_body); 78 | let match_exprs = get_all_match_expressions(function_body); 79 | let arenas = &function_body.arenas; 80 | for match_expr in match_exprs.iter() { 81 | if check_manual(db, match_expr, arenas, ManualLint::ManualOk) { 82 | diagnostics.push(PluginDiagnostic { 83 | stable_ptr: match_expr.stable_ptr.untyped(), 84 | message: ManualOk.diagnostic_message().to_owned(), 85 | severity: Severity::Warning, 86 | inner_span: None, 87 | }); 88 | } 89 | } 90 | for if_expr in if_exprs.iter() { 91 | if check_manual_if(db, if_expr, arenas, ManualLint::ManualOk) { 92 | diagnostics.push(PluginDiagnostic { 93 | stable_ptr: if_expr.stable_ptr.untyped(), 94 | message: ManualOk.diagnostic_message().to_owned(), 95 | severity: Severity::Warning, 96 | inner_span: None, 97 | }); 98 | } 99 | } 100 | } 101 | } 102 | 103 | /// Rewrites a manual implementation of ok 104 | #[tracing::instrument(skip_all, level = "trace")] 105 | pub fn fix_manual_ok<'db>( 106 | db: &'db dyn Database, 107 | node: SyntaxNode<'db>, 108 | ) -> Option> { 109 | Some(InternalFix { 110 | node, 111 | suggestion: fix_manual("ok", db, node), 112 | description: ManualOk.fix_message().unwrap().to_string(), 113 | import_addition_paths: None, 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /src/lints/performance/inefficient_while_comp.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::ids::ModuleItemId; 2 | use cairo_lang_defs::plugin::PluginDiagnostic; 3 | use cairo_lang_diagnostics::Severity; 4 | use cairo_lang_semantic::{Arenas, Condition, Expr, ExprWhile}; 5 | 6 | use crate::context::{CairoLintKind, Lint}; 7 | 8 | use crate::queries::{get_all_function_bodies, get_all_while_expressions}; 9 | use salsa::Database; 10 | 11 | pub struct InefficientWhileComparison; 12 | 13 | /// ## What it does 14 | /// 15 | /// Checks if the while loop exit condition is using relational (`<`, `<=`, `>=`, `>`) operators. 16 | /// 17 | /// ## Example 18 | /// 19 | /// ```cairo 20 | /// fn main() { 21 | /// let mut a = 1_u32; 22 | /// while a <= 10 { 23 | /// a += 1; 24 | /// } 25 | /// } 26 | /// ``` 27 | /// 28 | /// Can be optimized to: 29 | /// 30 | /// ```cairo 31 | /// fn main() { 32 | /// let mut a = 1_u32; 33 | /// while a != 10 { 34 | /// a += 1; 35 | /// } 36 | /// } 37 | /// ``` 38 | impl Lint for InefficientWhileComparison { 39 | fn allowed_name(&self) -> &'static str { 40 | "inefficient_while_comp" 41 | } 42 | 43 | fn diagnostic_message(&self) -> &'static str { 44 | "using [`<`, `<=`, `>=`, `>`] exit conditions is inefficient. Consider \ 45 | switching to `!=` or using ArrayTrait::multi_pop_front." 46 | } 47 | 48 | fn kind(&self) -> CairoLintKind { 49 | CairoLintKind::Performance 50 | } 51 | 52 | fn is_enabled(&self) -> bool { 53 | false 54 | } 55 | } 56 | 57 | // Match all types implementing PartialOrd 58 | const PARTIAL_ORD_PATTERNS: [&str; 4] = [ 59 | "PartialOrd::lt\"", 60 | "PartialOrd::le\"", 61 | "PartialOrd::gt\"", 62 | "PartialOrd::ge\"", 63 | ]; 64 | 65 | #[tracing::instrument(skip_all, level = "trace")] 66 | pub fn check_inefficient_while_comp<'db>( 67 | db: &'db dyn Database, 68 | item: &ModuleItemId<'db>, 69 | diagnostics: &mut Vec>, 70 | ) { 71 | let function_bodies = get_all_function_bodies(db, item); 72 | for function_body in function_bodies { 73 | let while_exprs = get_all_while_expressions(function_body); 74 | let arenas = &function_body.arenas; 75 | for while_expr in while_exprs.iter() { 76 | check_single_inefficient_while_comp(db, while_expr, diagnostics, arenas); 77 | } 78 | } 79 | } 80 | 81 | fn check_single_inefficient_while_comp<'db>( 82 | db: &'db dyn Database, 83 | while_expr: &ExprWhile<'db>, 84 | diagnostics: &mut Vec>, 85 | arenas: &Arenas<'db>, 86 | ) { 87 | // It might be a false positive, because there can be cases when: 88 | // - The rhs arguments is changed in the loop body 89 | // - The lhs argument can "skip" the moment where lhs == rhs 90 | if let Condition::BoolExpr(expr_cond) = while_expr.condition { 91 | check_expression(db, &arenas.exprs[expr_cond], diagnostics, arenas); 92 | } 93 | } 94 | 95 | fn check_expression<'db>( 96 | db: &'db dyn Database, 97 | expr: &Expr<'db>, 98 | diagnostics: &mut Vec>, 99 | arenas: &Arenas<'db>, 100 | ) { 101 | match expr { 102 | Expr::FunctionCall(func_call) => { 103 | let func_name = func_call.function.name(db); 104 | if PARTIAL_ORD_PATTERNS.iter().any(|p| func_name.ends_with(p)) { 105 | diagnostics.push(PluginDiagnostic { 106 | stable_ptr: func_call.stable_ptr.into(), 107 | message: InefficientWhileComparison.diagnostic_message().to_owned(), 108 | severity: Severity::Warning, 109 | inner_span: None, 110 | }); 111 | } 112 | } 113 | Expr::LogicalOperator(expr_logical) => { 114 | check_expression(db, &arenas.exprs[expr_logical.lhs], diagnostics, arenas); 115 | check_expression(db, &arenas.exprs[expr_logical.rhs], diagnostics, arenas); 116 | } 117 | _ => {} 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/duplicate_underscore_args/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{test_lint_diagnostics, test_lint_fixer}; 2 | 3 | const DUPLICATE_UNDERSCORE_ARGS_ALLOWED: &str = r#" 4 | #[allow(duplicate_underscore_args)] 5 | fn foo(a: u32, _a: u32) {} 6 | "#; 7 | 8 | const DUPLICATE_UNDERSCORE_ARGS2: &str = r#" 9 | fn foo(c: u32, _c: u32) {} 10 | "#; 11 | 12 | const DUPLICATE_UNDERSCORE_LONGER_ARGS: &str = r#" 13 | fn foo(test: u32, _test: u32) {} 14 | "#; 15 | 16 | const DUPLICATE_UNDERSCORE_LONGER_ARGS2: &str = r#" 17 | fn foo(darth: u32, _darth: u32) {} 18 | "#; 19 | 20 | const DUPLICATE_UNDERSCORE_LONGER_ARGS3: &str = r#" 21 | fn foo(stark: u32, _stark: u32) {} 22 | "#; 23 | 24 | const DUPLICATE_UNDERSCORE_LONGER_ARGS4: &str = r#" 25 | fn foo(_test: u32, test: u32) {} 26 | "#; 27 | 28 | #[test] 29 | fn duplicate_underscore_args_allowed_diagnostics() { 30 | test_lint_diagnostics!(DUPLICATE_UNDERSCORE_ARGS_ALLOWED, @r#" 31 | "#); 32 | } 33 | 34 | #[test] 35 | fn duplicate_underscore_args_allowed_fixer() { 36 | test_lint_fixer!(DUPLICATE_UNDERSCORE_ARGS_ALLOWED, @r#" 37 | #[allow(duplicate_underscore_args)] 38 | fn foo(a: u32, _a: u32) {} 39 | "#); 40 | } 41 | 42 | #[test] 43 | fn duplicate_underscore_args2_diagnostics() { 44 | test_lint_diagnostics!(DUPLICATE_UNDERSCORE_ARGS2, @r" 45 | Plugin diagnostic: duplicate arguments, having another argument having almost the same name makes code comprehension and documentation more difficult 46 | --> lib.cairo:2:16 47 | fn foo(c: u32, _c: u32) {} 48 | ^^ 49 | "); 50 | } 51 | 52 | #[test] 53 | fn duplicate_underscore_args2_fixer() { 54 | test_lint_fixer!(DUPLICATE_UNDERSCORE_ARGS2, @r#" 55 | fn foo(c: u32, _c: u32) {} 56 | "#); 57 | } 58 | 59 | #[test] 60 | fn duplicate_underscore_longer_args_diagnostics() { 61 | test_lint_diagnostics!(DUPLICATE_UNDERSCORE_LONGER_ARGS, @r" 62 | Plugin diagnostic: duplicate arguments, having another argument having almost the same name makes code comprehension and documentation more difficult 63 | --> lib.cairo:2:19 64 | fn foo(test: u32, _test: u32) {} 65 | ^^^^^ 66 | "); 67 | } 68 | 69 | #[test] 70 | fn duplicate_underscore_longer_args_fixer() { 71 | test_lint_fixer!(DUPLICATE_UNDERSCORE_LONGER_ARGS, @r#" 72 | fn foo(test: u32, _test: u32) {} 73 | "#); 74 | } 75 | 76 | #[test] 77 | fn duplicate_underscore_longer_args2_diagnostics() { 78 | test_lint_diagnostics!(DUPLICATE_UNDERSCORE_LONGER_ARGS2, @r" 79 | Plugin diagnostic: duplicate arguments, having another argument having almost the same name makes code comprehension and documentation more difficult 80 | --> lib.cairo:2:20 81 | fn foo(darth: u32, _darth: u32) {} 82 | ^^^^^^ 83 | "); 84 | } 85 | 86 | #[test] 87 | fn duplicate_underscore_longer_args2_fixer() { 88 | test_lint_fixer!(DUPLICATE_UNDERSCORE_LONGER_ARGS2, @r#" 89 | fn foo(darth: u32, _darth: u32) {} 90 | "#); 91 | } 92 | 93 | #[test] 94 | fn duplicate_underscore_longer_args3_diagnostics() { 95 | test_lint_diagnostics!(DUPLICATE_UNDERSCORE_LONGER_ARGS3, @r" 96 | Plugin diagnostic: duplicate arguments, having another argument having almost the same name makes code comprehension and documentation more difficult 97 | --> lib.cairo:2:20 98 | fn foo(stark: u32, _stark: u32) {} 99 | ^^^^^^ 100 | "); 101 | } 102 | 103 | #[test] 104 | fn duplicate_underscore_longer_args3_fixer() { 105 | test_lint_fixer!(DUPLICATE_UNDERSCORE_LONGER_ARGS3, @r#" 106 | fn foo(stark: u32, _stark: u32) {} 107 | "#); 108 | } 109 | 110 | #[test] 111 | fn duplicate_underscore_longer_args4_diagnostics() { 112 | test_lint_diagnostics!(DUPLICATE_UNDERSCORE_LONGER_ARGS4, @r" 113 | Plugin diagnostic: duplicate arguments, having another argument having almost the same name makes code comprehension and documentation more difficult 114 | --> lib.cairo:2:20 115 | fn foo(_test: u32, test: u32) {} 116 | ^^^^ 117 | "); 118 | } 119 | 120 | #[test] 121 | fn duplicate_underscore_longer_args4_fixer() { 122 | test_lint_fixer!(DUPLICATE_UNDERSCORE_LONGER_ARGS4, @r#" 123 | fn foo(_test: u32, test: u32) {} 124 | "#); 125 | } 126 | -------------------------------------------------------------------------------- /tests/unused_variables/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{test_lint_diagnostics, test_lint_fixer}; 2 | 3 | const ONE_UNUSED_VARIABLE: &str = r#" 4 | fn main() { 5 | let a: Option = Option::Some(1); 6 | } 7 | "#; 8 | 9 | const TWO_UNUSED_VARIABLES: &str = r#" 10 | fn main() { 11 | let a: Option = Option::Some(1); 12 | let b = 1; 13 | } 14 | "#; 15 | 16 | const PLENTY_UNUSED_VARIABLES: &str = r#" 17 | fn main() { 18 | let used: Option = Option::Some(1); 19 | let b = 1; 20 | { 21 | let c = 1_u32; 22 | } 23 | if true { 24 | let _avoid_collapsible = 1_u32; 25 | if false { 26 | let d = 3_u32; 27 | } else { 28 | let e = false; 29 | } 30 | let f: Array = array![]; 31 | } else { 32 | let g: Option = Option::None; 33 | match used { 34 | Option::Some(not_used) => 1_u32, 35 | Option::None => 2_u32, 36 | }; 37 | } 38 | } 39 | "#; 40 | 41 | #[test] 42 | fn one_unused_variable_diagnostics() { 43 | test_lint_diagnostics!(ONE_UNUSED_VARIABLE, @r" 44 | Unused variable. Consider ignoring by prefixing with `_`. 45 | --> lib.cairo:3:9 46 | let a: Option = Option::Some(1); 47 | ^ 48 | "); 49 | } 50 | 51 | #[test] 52 | fn one_unused_variable_fixer() { 53 | test_lint_fixer!(ONE_UNUSED_VARIABLE, @r" 54 | fn main() { 55 | let a: Option = Option::Some(1); 56 | } 57 | "); 58 | } 59 | 60 | #[test] 61 | fn two_unused_variables_diagnostics() { 62 | test_lint_diagnostics!(TWO_UNUSED_VARIABLES, @r" 63 | Unused variable. Consider ignoring by prefixing with `_`. 64 | --> lib.cairo:3:9 65 | let a: Option = Option::Some(1); 66 | ^ 67 | Unused variable. Consider ignoring by prefixing with `_`. 68 | --> lib.cairo:4:9 69 | let b = 1; 70 | ^ 71 | "); 72 | } 73 | 74 | #[test] 75 | fn two_unused_variables_fixer() { 76 | test_lint_fixer!(TWO_UNUSED_VARIABLES, @r" 77 | fn main() { 78 | let a: Option = Option::Some(1); 79 | let b = 1; 80 | } 81 | "); 82 | } 83 | 84 | #[test] 85 | fn plenty_unused_variables_diagnostics() { 86 | test_lint_diagnostics!(PLENTY_UNUSED_VARIABLES, @r" 87 | Unused variable. Consider ignoring by prefixing with `_`. 88 | --> lib.cairo:6:13 89 | let c = 1_u32; 90 | ^ 91 | Unused variable. Consider ignoring by prefixing with `_`. 92 | --> lib.cairo:11:17 93 | let d = 3_u32; 94 | ^ 95 | Unused variable. Consider ignoring by prefixing with `_`. 96 | --> lib.cairo:13:17 97 | let e = false; 98 | ^ 99 | Unused variable. Consider ignoring by prefixing with `_`. 100 | --> lib.cairo:15:13 101 | let f: Array = array![]; 102 | ^ 103 | Unused variable. Consider ignoring by prefixing with `_`. 104 | --> lib.cairo:19:26 105 | Option::Some(not_used) => 1_u32, 106 | ^^^^^^^^ 107 | Unused variable. Consider ignoring by prefixing with `_`. 108 | --> lib.cairo:17:13 109 | let g: Option = Option::None; 110 | ^ 111 | Unused variable. Consider ignoring by prefixing with `_`. 112 | --> lib.cairo:4:9 113 | let b = 1; 114 | ^ 115 | "); 116 | } 117 | 118 | #[test] 119 | fn plenty_unused_variables_fixer() { 120 | test_lint_fixer!(PLENTY_UNUSED_VARIABLES, @r" 121 | fn main() { 122 | let used: Option = Option::Some(1); 123 | let b = 1; 124 | { 125 | let c = 1_u32; 126 | } 127 | if true { 128 | let _avoid_collapsible = 1_u32; 129 | if false { 130 | let d = 3_u32; 131 | } else { 132 | let e = false; 133 | } 134 | let f: Array = array![]; 135 | } else { 136 | let g: Option = Option::None; 137 | match used { 138 | Option::Some(not_used) => 1_u32, 139 | Option::None => 2_u32, 140 | }; 141 | } 142 | } 143 | "); 144 | } 145 | -------------------------------------------------------------------------------- /tests/collapsible_match/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{test_lint_diagnostics, test_lint_fixer}; 2 | 3 | const COLLAPSIBLE_MATCH_BASIC: &str = r#" 4 | fn func(opt: Option>) { 5 | let _n = match opt { 6 | Some(n) => match n { 7 | Ok(n) => Some(n), 8 | _ => None, 9 | }, 10 | None => None, 11 | }; 12 | } 13 | "#; 14 | 15 | const COLLAPSIBLE_MATCH_DIFFERENT_ORDER_OF_ARMS: &str = r#" 16 | fn func(opt: Option>) { 17 | let _n = match opt { 18 | None => None, 19 | Some(n) => match n { 20 | _ => None, 21 | Ok(n) => Some(n), 22 | }, 23 | }; 24 | } 25 | "#; 26 | 27 | const COLLAPSIBLE_MATCH_MIXED_ORDER_OF_ARMS: &str = r#" 28 | fn func(opt: Option>) { 29 | let _n = match opt { 30 | None => None, 31 | Some(n) => match n { 32 | Ok(n) => Some(n), 33 | _ => None, 34 | }, 35 | }; 36 | } 37 | "#; 38 | 39 | const NON_COLLAPSIBLE_MATCH: &str = r#" 40 | fn func(opt: Option>) { 41 | let _n = match opt { 42 | Some(n) => match n { 43 | Ok(n) => Some(n), 44 | _ => Some(0), 45 | }, 46 | None => None, 47 | }; 48 | } 49 | "#; 50 | 51 | #[test] 52 | fn test_collapsible_match_basic_diagnostics() { 53 | test_lint_diagnostics!( 54 | COLLAPSIBLE_MATCH_BASIC, @r" 55 | Plugin diagnostic: Nested `match` statements can be collapsed into a single `match` statement. 56 | --> lib.cairo:3:14-9:5 57 | let _n = match opt { 58 | ______________^ 59 | | ... 60 | | }; 61 | |_____^ 62 | " 63 | ); 64 | } 65 | 66 | #[test] 67 | fn test_collapsible_match_basic_fixer() { 68 | test_lint_fixer!( 69 | COLLAPSIBLE_MATCH_BASIC, 70 | @r" 71 | fn func(opt: Option>) { 72 | let _n = match opt { 73 | Some(Ok(n)) => Some(n), 74 | _ => None, 75 | }; 76 | } 77 | " 78 | ); 79 | } 80 | 81 | #[test] 82 | fn test_collapsible_match_different_order_of_arms_diagnostics() { 83 | test_lint_diagnostics!( 84 | COLLAPSIBLE_MATCH_DIFFERENT_ORDER_OF_ARMS, @r" 85 | Plugin diagnostic: Nested `match` statements can be collapsed into a single `match` statement. 86 | --> lib.cairo:3:14-9:5 87 | let _n = match opt { 88 | ______________^ 89 | | ... 90 | | }; 91 | |_____^ 92 | " 93 | ); 94 | } 95 | 96 | #[test] 97 | fn test_collapsible_match_different_order_of_arms_fixer() { 98 | test_lint_fixer!( 99 | COLLAPSIBLE_MATCH_DIFFERENT_ORDER_OF_ARMS, 100 | @r" 101 | fn func(opt: Option>) { 102 | let _n = match opt { 103 | Some(Ok(n)) => Some(n), 104 | _ => None, 105 | }; 106 | } 107 | " 108 | ); 109 | } 110 | 111 | #[test] 112 | fn test_collapsible_match_mixed_order_of_arms_diagnostics() { 113 | test_lint_diagnostics!( 114 | COLLAPSIBLE_MATCH_MIXED_ORDER_OF_ARMS, @r" 115 | Plugin diagnostic: Nested `match` statements can be collapsed into a single `match` statement. 116 | --> lib.cairo:3:14-9:5 117 | let _n = match opt { 118 | ______________^ 119 | | ... 120 | | }; 121 | |_____^ 122 | " 123 | ); 124 | } 125 | 126 | #[test] 127 | fn test_collapsible_match_mixed_order_of_arms_fixer() { 128 | test_lint_fixer!( 129 | COLLAPSIBLE_MATCH_MIXED_ORDER_OF_ARMS, 130 | @r" 131 | fn func(opt: Option>) { 132 | let _n = match opt { 133 | Some(Ok(n)) => Some(n), 134 | _ => None, 135 | }; 136 | } 137 | " 138 | ); 139 | } 140 | 141 | #[test] 142 | fn test_non_collapsible_match_diagnostics() { 143 | test_lint_diagnostics!( 144 | NON_COLLAPSIBLE_MATCH, @"" 145 | ); 146 | } 147 | 148 | #[test] 149 | fn test_non_collapsible_fixer() { 150 | test_lint_fixer!( 151 | NON_COLLAPSIBLE_MATCH, 152 | @r" 153 | fn func(opt: Option>) { 154 | let _n = match opt { 155 | Some(n) => match n { 156 | Ok(n) => Some(n), 157 | _ => Some(0), 158 | }, 159 | None => None, 160 | }; 161 | } 162 | " 163 | ); 164 | } 165 | -------------------------------------------------------------------------------- /src/lints/unit_return_type.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::{ids::ModuleItemId, plugin::PluginDiagnostic}; 2 | use cairo_lang_diagnostics::Severity; 3 | use cairo_lang_semantic::items::function_with_body::FunctionWithBodySemantic; 4 | use cairo_lang_syntax::node::TypedSyntaxNode; 5 | use cairo_lang_syntax::node::{ 6 | SyntaxNode, TypedStablePtr, 7 | ast::{FunctionSignature, OptionReturnTypeClause}, 8 | }; 9 | 10 | use crate::fixer::InternalFix; 11 | use crate::{ 12 | context::{CairoLintKind, Lint}, 13 | queries::get_all_checkable_functions, 14 | }; 15 | use salsa::Database; 16 | 17 | pub struct UnitReturnType; 18 | 19 | /// ## What it does 20 | /// 21 | /// Detects if the function has a unit return type, which is not needed to be specified. 22 | /// 23 | /// ## Example 24 | /// 25 | /// ```cairo 26 | /// fn foo() -> () { 27 | /// println!("Hello, world!"); 28 | /// } 29 | /// ``` 30 | /// 31 | /// Can be simplified to just: 32 | /// 33 | /// ```cairo 34 | /// fn foo() { 35 | /// println!("Hello, world!"); 36 | /// } 37 | /// ``` 38 | impl Lint for UnitReturnType { 39 | fn allowed_name(&self) -> &'static str { 40 | "unit_return_type" 41 | } 42 | 43 | fn diagnostic_message(&self) -> &'static str { 44 | "unnecessary declared unit return type `()`" 45 | } 46 | 47 | fn kind(&self) -> CairoLintKind { 48 | CairoLintKind::UnitReturnType 49 | } 50 | 51 | fn has_fixer(&self) -> bool { 52 | true 53 | } 54 | 55 | fn fix<'db>(&self, db: &'db dyn Database, node: SyntaxNode<'db>) -> Option> { 56 | fix_unit_return_type(db, node) 57 | } 58 | 59 | fn fix_message(&self) -> Option<&'static str> { 60 | Some("Remove explicit unit return type") 61 | } 62 | } 63 | 64 | #[tracing::instrument(skip_all, level = "trace")] 65 | pub fn check_unit_return_type<'db>( 66 | db: &'db dyn Database, 67 | item: &ModuleItemId<'db>, 68 | diagnostics: &mut Vec>, 69 | ) { 70 | let functions = get_all_checkable_functions(db, item); 71 | for function in functions { 72 | let function_signature = db.function_with_body_signature(function).unwrap(); 73 | let return_type = function_signature.return_type; 74 | 75 | // Check if the return type is unit. 76 | if return_type.is_unit(db) { 77 | let ast_function_signature = function_signature.stable_ptr.lookup(db); 78 | 79 | // Checks if the return type is explicitly declared as `()`. 80 | if let OptionReturnTypeClause::ReturnTypeClause(_) = ast_function_signature.ret_ty(db) { 81 | diagnostics.push(PluginDiagnostic { 82 | stable_ptr: function_signature.stable_ptr.untyped(), 83 | message: UnitReturnType.diagnostic_message().to_string(), 84 | severity: Severity::Warning, 85 | inner_span: None, 86 | }); 87 | } 88 | } 89 | } 90 | } 91 | 92 | #[tracing::instrument(skip_all, level = "trace")] 93 | pub fn fix_unit_return_type<'db>( 94 | db: &'db dyn Database, 95 | node: SyntaxNode<'db>, 96 | ) -> Option> { 97 | let function_signature = FunctionSignature::from_syntax_node(db, node); 98 | let return_type_clause = function_signature.ret_ty(db); 99 | if let OptionReturnTypeClause::ReturnTypeClause(type_clause) = return_type_clause { 100 | let fixed = node.get_text(db); 101 | let type_clause_text = type_clause.as_syntax_node().get_text(db); 102 | let fixed = fixed.replace(type_clause_text, ""); 103 | 104 | if type_clause_text.ends_with(" ") { 105 | return Some(InternalFix { 106 | node, 107 | suggestion: fixed, 108 | description: UnitReturnType.fix_message().unwrap().to_string(), 109 | import_addition_paths: None, 110 | }); 111 | } 112 | 113 | // In case the `()` type doesn't have a space after it, like `fn foo() -> ();`, we trim the end. 114 | return Some(InternalFix { 115 | node, 116 | suggestion: fixed.trim_end().to_string(), 117 | description: UnitReturnType.fix_message().unwrap().to_string(), 118 | import_addition_paths: None, 119 | }); 120 | } 121 | panic!("Expected a function signature with a return type clause."); 122 | } 123 | -------------------------------------------------------------------------------- /website/.vitepress/config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress"; 2 | import * as syntaxes from "./syntaxes.mjs"; 3 | import { getLintsSidebar } from "./lints_sidebar.mjs"; 4 | 5 | const base = "/cairo-lint/"; 6 | const absoluteBase = `https://docs.swmansion.com${base}`; 7 | const lang = "en-US"; 8 | 9 | const getSidebar = () => ({ 10 | "/docs": [ 11 | { 12 | text: "Overview", 13 | items: [p("Introduction", "/docs"), p("Default profile", "docs/default_profile")], 14 | }, 15 | { 16 | text: "Lints", 17 | items: getLintsSidebar(), 18 | }, 19 | ], 20 | }); 21 | 22 | const telegramIcon = ` 23 | 24 | Telegram 25 | 26 | 27 | `; 28 | 29 | export default defineConfig({ 30 | title: "Cairo lint", 31 | description: "Cairo lint is a static code analyzer tool for Cairo language.", 32 | lang, 33 | base, 34 | 35 | head: [ 36 | ["meta", { httpEquiv: "Content-Language", content: lang }], 37 | ["link", { rel: "icon", href: `${base}favicon.svg`, type: "image/x-icon" }], 38 | ["link", { rel: "apple-touch-icon", href: `${base}apple-touch-icon.png` }], 39 | ["meta", { name: "apple-mobile-web-app-title", content: "Cairo lint" }], 40 | ["meta", { name: "twitter:card", content: "summary_large_image" }], 41 | ["meta", { name: "twitter:site", content: "@swmansionxyz" }], 42 | ["meta", { name: "twitter:creator", content: "@_Tobysmy_" }], 43 | [ 44 | "meta", 45 | { 46 | property: "og:title", 47 | content: "Cairo lint", 48 | }, 49 | ], 50 | [ 51 | "meta", 52 | { 53 | property: "og:description", 54 | content: "Cairo lint is a static code analyzer tool for Cairo language.", 55 | }, 56 | ], 57 | [ 58 | "meta", 59 | { 60 | property: "og:image:alt", 61 | content: "Cairo lint is a static code analyzer tool for Cairo language.", 62 | }, 63 | ], 64 | ["meta", { property: "og:image:type", content: "image/png" }], 65 | ["meta", { property: "og:image:width", content: "1280" }], 66 | ["meta", { property: "og:image:height", content: "640" }], 67 | ], 68 | 69 | lastUpdated: true, 70 | 71 | themeConfig: { 72 | logo: { 73 | light: "/favicon.svg", 74 | dark: "/favicon.svg", 75 | alt: "Cairo lint", 76 | }, 77 | siteTitle: "Cairo lint", 78 | 79 | nav: [ 80 | { text: "Download", link: "https://docs.swmansion.com/scarb/download.html" }, 81 | { text: "Documentation", link: "/docs" }, 82 | ], 83 | 84 | sidebar: getSidebar(), 85 | 86 | socialLinks: [ 87 | { icon: "github", link: "https://github.com/software-mansion/cairo-lint" }, 88 | { icon: "twitter", link: "https://twitter.com/swmansionxyz" }, 89 | { 90 | icon: { 91 | svg: telegramIcon, 92 | }, 93 | ariaLabel: "Telegram", 94 | link: "https://t.me/cairolint", 95 | }, 96 | ], 97 | 98 | editLink: { 99 | pattern: "https://github.com/software-mansion/cairo-lint/tree/main/website/:path", 100 | text: "Edit this page on GitHub", 101 | }, 102 | 103 | search: { 104 | provider: "local", 105 | }, 106 | }, 107 | 108 | sitemap: { 109 | hostname: absoluteBase, 110 | }, 111 | 112 | markdown: { 113 | languages: [syntaxes.cairo], 114 | }, 115 | }); 116 | 117 | function p(text, link) { 118 | return { text, link }; 119 | } 120 | -------------------------------------------------------------------------------- /src/lints/redundant_into.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::ids::{GenericTypeId, ModuleItemId}; 2 | use cairo_lang_defs::plugin::PluginDiagnostic; 3 | use cairo_lang_diagnostics::Severity; 4 | use cairo_lang_semantic::items::functions::GenericFunctionId; 5 | use cairo_lang_semantic::{ 6 | Arenas, ExprFunctionCall, ExprFunctionCallArg, GenericArgumentId, TypeId, TypeLongId, 7 | }; 8 | use cairo_lang_syntax::node::TypedStablePtr; 9 | use salsa::Database; 10 | 11 | use crate::LinterGroup; 12 | use crate::context::{CairoLintKind, Lint}; 13 | use crate::queries::{get_all_function_bodies, get_all_function_calls}; 14 | 15 | pub struct RedundantInto; 16 | 17 | /// ## What it does 18 | /// 19 | /// Detects redundant calls to `into()` or `try_into()` where the input and output 20 | /// types are the same, i.e., the conversion is a no-op and can be removed. 21 | /// 22 | /// ## Example 23 | /// 24 | /// ```cairo 25 | /// fn f(x: u128) -> u128 { 26 | /// // redundant - `x` is already an u128 27 | /// x.into() 28 | /// } 29 | /// ``` 30 | impl Lint for RedundantInto { 31 | fn allowed_name(&self) -> &'static str { 32 | "redundant_into" 33 | } 34 | 35 | fn diagnostic_message(&self) -> &'static str { 36 | "Redundant conversion: input and output types are the same." 37 | } 38 | 39 | fn kind(&self) -> CairoLintKind { 40 | CairoLintKind::RedundantInto 41 | } 42 | } 43 | 44 | #[tracing::instrument(skip_all, level = "trace")] 45 | pub fn check_redundant_into<'db>( 46 | db: &'db dyn Database, 47 | item: &ModuleItemId<'db>, 48 | diagnostics: &mut Vec>, 49 | ) { 50 | let function_bodies = get_all_function_bodies(db, item); 51 | for function_body in function_bodies.iter() { 52 | let arenas = &function_body.arenas; 53 | let function_call_exprs = get_all_function_calls(function_body); 54 | for function_call_expr in function_call_exprs { 55 | check_single_redundant_into(db, &function_call_expr, arenas, diagnostics); 56 | } 57 | } 58 | } 59 | 60 | fn check_single_redundant_into<'db>( 61 | db: &'db dyn Database, 62 | expr_func: &ExprFunctionCall<'db>, 63 | arenas: &Arenas<'db>, 64 | diagnostics: &mut Vec>, 65 | ) { 66 | let corelib_context = db.corelib_context(); 67 | let into_fn_id = corelib_context.get_into_trait_function_id(); 68 | let try_into_fn_id = corelib_context.get_try_into_trait_function_id(); 69 | 70 | let GenericFunctionId::Impl(impl_generic_func_id) = 71 | expr_func.function.get_concrete(db).generic_function 72 | else { 73 | return; 74 | }; 75 | 76 | let function = impl_generic_func_id.function; 77 | 78 | let target_ty: TypeId = if function == into_fn_id { 79 | expr_func.ty 80 | } else if function == try_into_fn_id { 81 | if let Some(ok_ty) = result_ok_type(db, expr_func.ty) { 82 | ok_ty 83 | } else { 84 | return; 85 | } 86 | } else { 87 | return; 88 | }; 89 | 90 | let Some(first_arg) = expr_func.args.first() else { 91 | return; 92 | }; 93 | 94 | let input_ty: TypeId = if let ExprFunctionCallArg::Value(expr_id) = first_arg { 95 | arenas.exprs[*expr_id].ty() 96 | } else { 97 | // Not possible, since the Into/TryInto function doesn't take mutable args 98 | return; 99 | }; 100 | 101 | if input_ty == target_ty { 102 | diagnostics.push(PluginDiagnostic { 103 | stable_ptr: expr_func.stable_ptr.untyped(), 104 | message: RedundantInto.diagnostic_message().to_string(), 105 | severity: Severity::Warning, 106 | inner_span: None, 107 | }); 108 | } 109 | } 110 | 111 | /// Extracts T from `core::option::Option::` 112 | fn result_ok_type<'db>(db: &'db dyn Database, ty: TypeId<'db>) -> Option> { 113 | if let TypeLongId::Concrete(conc) = ty.long(db) { 114 | let generic_ty = conc.generic_type(db); 115 | let corelib_context = db.corelib_context(); 116 | let option_enum_id = corelib_context.get_option_enum_id(); 117 | let type_id = if let GenericTypeId::Enum(enum_id) = generic_ty { 118 | enum_id 119 | } else { 120 | return None; 121 | }; 122 | 123 | if type_id == option_enum_id { 124 | let mut args = conc.generic_args(db).into_iter(); 125 | if let Some(GenericArgumentId::Type(ok_ty)) = args.next() { 126 | return Some(ok_ty); 127 | } 128 | } 129 | } 130 | None 131 | } 132 | -------------------------------------------------------------------------------- /src/lints/manual/manual_ok_or.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::ids::ModuleItemId; 2 | use cairo_lang_defs::plugin::PluginDiagnostic; 3 | use cairo_lang_diagnostics::Severity; 4 | use cairo_lang_syntax::node::{ 5 | SyntaxNode, TypedStablePtr, TypedSyntaxNode, 6 | ast::{ExprIf, ExprMatch}, 7 | kind::SyntaxKind, 8 | }; 9 | 10 | use crate::{ 11 | context::CairoLintKind, 12 | fixer::InternalFix, 13 | queries::{get_all_function_bodies, get_all_if_expressions, get_all_match_expressions}, 14 | }; 15 | use crate::{ 16 | context::Lint, 17 | lints::manual::{ 18 | ManualLint, check_manual, check_manual_if, 19 | helpers::{expr_if_get_var_name_and_err, expr_match_get_var_name_and_err}, 20 | }, 21 | }; 22 | use salsa::Database; 23 | 24 | pub struct ManualOkOr; 25 | 26 | /// ## What it does 27 | /// 28 | /// Checks for manual implementations of ok_or. 29 | /// 30 | /// ## Example 31 | /// 32 | /// ```cairo 33 | /// fn main() { 34 | /// let foo: Option = Option::None; 35 | /// let _foo = match foo { 36 | /// Option::Some(v) => Result::Ok(v), 37 | /// Option::None => Result::Err('this is an err'), 38 | /// }; 39 | /// } 40 | /// ``` 41 | /// 42 | /// Can be rewritten as: 43 | /// 44 | /// ```cairo 45 | /// fn main() { 46 | /// let foo: Option = Option::None; 47 | /// let _foo = foo.ok_or('this is an err'); 48 | /// } 49 | /// ``` 50 | impl Lint for ManualOkOr { 51 | fn allowed_name(&self) -> &'static str { 52 | "manual_ok_or" 53 | } 54 | 55 | fn diagnostic_message(&self) -> &'static str { 56 | "Manual match for Option detected. Consider using ok_or instead" 57 | } 58 | 59 | fn kind(&self) -> CairoLintKind { 60 | CairoLintKind::ManualOkOr 61 | } 62 | 63 | fn has_fixer(&self) -> bool { 64 | true 65 | } 66 | 67 | fn fix<'db>(&self, db: &'db dyn Database, node: SyntaxNode<'db>) -> Option> { 68 | fix_manual_ok_or(db, node) 69 | } 70 | 71 | fn fix_message(&self) -> Option<&'static str> { 72 | Some("Replace manual conversion with `ok_or()` method") 73 | } 74 | } 75 | 76 | #[tracing::instrument(skip_all, level = "trace")] 77 | pub fn check_manual_ok_or<'db>( 78 | db: &'db dyn Database, 79 | item: &ModuleItemId<'db>, 80 | diagnostics: &mut Vec>, 81 | ) { 82 | let function_bodies = get_all_function_bodies(db, item); 83 | for function_body in function_bodies.iter() { 84 | let if_exprs = get_all_if_expressions(function_body); 85 | let match_exprs = get_all_match_expressions(function_body); 86 | let arenas = &function_body.arenas; 87 | for match_expr in match_exprs.iter() { 88 | if check_manual(db, match_expr, arenas, ManualLint::ManualOkOr) { 89 | diagnostics.push(PluginDiagnostic { 90 | stable_ptr: match_expr.stable_ptr.untyped(), 91 | message: ManualOkOr.diagnostic_message().to_owned(), 92 | severity: Severity::Warning, 93 | inner_span: None, 94 | }); 95 | } 96 | } 97 | for if_expr in if_exprs.iter() { 98 | if check_manual_if(db, if_expr, arenas, ManualLint::ManualOkOr) { 99 | diagnostics.push(PluginDiagnostic { 100 | stable_ptr: if_expr.stable_ptr.untyped(), 101 | message: ManualOkOr.diagnostic_message().to_owned(), 102 | severity: Severity::Warning, 103 | inner_span: None, 104 | }); 105 | } 106 | } 107 | } 108 | } 109 | 110 | /// Rewrites a manual implementation of ok_or 111 | #[tracing::instrument(skip_all, level = "trace")] 112 | pub fn fix_manual_ok_or<'db>( 113 | db: &'db dyn Database, 114 | node: SyntaxNode<'db>, 115 | ) -> Option> { 116 | let fix = match node.kind(db) { 117 | SyntaxKind::ExprMatch => { 118 | let expr_match = ExprMatch::from_syntax_node(db, node); 119 | 120 | let (option_var_name, none_arm_err) = 121 | expr_match_get_var_name_and_err(expr_match, db, 1); 122 | 123 | format!("{}.ok_or({none_arm_err})", option_var_name.trim_end()) 124 | } 125 | SyntaxKind::ExprIf => { 126 | let expr_if = ExprIf::from_syntax_node(db, node); 127 | 128 | let (option_var_name, err) = expr_if_get_var_name_and_err(expr_if, db); 129 | 130 | format!("{}.ok_or({})", option_var_name.trim_end(), err) 131 | } 132 | _ => panic!("SyntaxKind should be either ExprIf or ExprMatch"), 133 | }; 134 | Some(InternalFix { 135 | node, 136 | suggestion: fix, 137 | description: ManualOkOr.fix_message().unwrap().to_string(), 138 | import_addition_paths: None, 139 | }) 140 | } 141 | -------------------------------------------------------------------------------- /src/lints/ifs/ifs_same_cond.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::ids::ModuleItemId; 2 | use cairo_lang_defs::plugin::PluginDiagnostic; 3 | use cairo_lang_diagnostics::Severity; 4 | use cairo_lang_semantic::{Arenas, Condition, Expr, ExprFunctionCall, ExprFunctionCallArg, ExprIf}; 5 | use cairo_lang_syntax::node::{TypedStablePtr, TypedSyntaxNode}; 6 | use if_chain::if_chain; 7 | 8 | use crate::context::{CairoLintKind, Lint}; 9 | 10 | use crate::queries::{get_all_function_bodies, get_all_if_expressions}; 11 | use salsa::Database; 12 | 13 | pub struct DuplicateIfCondition; 14 | 15 | /// ## What it does 16 | /// 17 | /// Checks for consecutive `if` expressions with the same condition. 18 | /// 19 | /// ## Example 20 | /// 21 | /// ```cairo 22 | /// fn main() { 23 | /// let a = 1; 24 | /// let b = 1; 25 | /// if a == b { 26 | /// println!("a is equal to b"); 27 | /// } else if a == b { 28 | /// println!("a is equal to b"); 29 | /// } 30 | /// } 31 | /// ``` 32 | /// 33 | /// Could be rewritten as just: 34 | /// 35 | /// ```cairo 36 | /// fn main() { 37 | /// let a = 1; 38 | /// let b = 1; 39 | /// if a == b { 40 | /// println!("a is equal to b"); 41 | /// } 42 | /// } 43 | /// ``` 44 | impl Lint for DuplicateIfCondition { 45 | fn allowed_name(&self) -> &'static str { 46 | "ifs_same_cond" 47 | } 48 | 49 | fn diagnostic_message(&self) -> &'static str { 50 | "Consecutive `if` with the same condition found." 51 | } 52 | 53 | fn kind(&self) -> CairoLintKind { 54 | CairoLintKind::DuplicateIfCondition 55 | } 56 | } 57 | 58 | #[tracing::instrument(skip_all, level = "trace")] 59 | pub fn check_duplicate_if_condition<'db>( 60 | db: &'db dyn Database, 61 | item: &ModuleItemId<'db>, 62 | diagnostics: &mut Vec>, 63 | ) { 64 | let function_bodies = get_all_function_bodies(db, item); 65 | for function_body in function_bodies.iter() { 66 | let if_exprs = get_all_if_expressions(function_body); 67 | let arenas = &function_body.arenas; 68 | for if_expr in if_exprs.iter() { 69 | check_single_duplicate_if_condition(db, if_expr, arenas, diagnostics); 70 | } 71 | } 72 | } 73 | 74 | fn check_single_duplicate_if_condition<'db>( 75 | db: &'db dyn Database, 76 | if_expr: &ExprIf<'db>, 77 | arenas: &Arenas<'db>, 78 | diagnostics: &mut Vec>, 79 | ) { 80 | let cond_expr = match &if_expr.conditions.first() { 81 | Some(Condition::BoolExpr(expr_id)) => &arenas.exprs[*expr_id], 82 | Some(Condition::Let(expr_id, _patterns)) => &arenas.exprs[*expr_id], 83 | _ => return, 84 | }; 85 | 86 | if_chain! { 87 | if let Expr::FunctionCall(func_call) = cond_expr; 88 | if ensure_no_ref_arg(arenas, func_call); 89 | then { 90 | return; 91 | } 92 | } 93 | 94 | let mut current_block = if_expr.else_block; 95 | let if_condition_text = cond_expr 96 | .stable_ptr() 97 | .lookup(db) 98 | .as_syntax_node() 99 | .get_text(db); 100 | 101 | while let Some(expr_id) = current_block { 102 | if let Expr::If(else_if_block) = &arenas.exprs[expr_id] { 103 | current_block = else_if_block.else_block; 104 | let else_if_cond = match &else_if_block.conditions.first() { 105 | Some(Condition::BoolExpr(expr_id)) => &arenas.exprs[*expr_id], 106 | Some(Condition::Let(expr_id, _patterns)) => &arenas.exprs[*expr_id], 107 | _ => continue, 108 | }; 109 | 110 | if_chain! { 111 | if let Expr::FunctionCall(func_call) = else_if_cond; 112 | if ensure_no_ref_arg(arenas, func_call); 113 | then { 114 | continue; 115 | } 116 | } 117 | 118 | let else_if_condition_text = else_if_cond 119 | .stable_ptr() 120 | .lookup(db) 121 | .as_syntax_node() 122 | .get_text(db); 123 | 124 | if if_condition_text == else_if_condition_text { 125 | diagnostics.push(PluginDiagnostic { 126 | stable_ptr: if_expr.stable_ptr.untyped(), 127 | message: DuplicateIfCondition.diagnostic_message().to_string(), 128 | severity: Severity::Warning, 129 | inner_span: None, 130 | }); 131 | break; 132 | } 133 | } else { 134 | break; 135 | } 136 | } 137 | } 138 | 139 | fn ensure_no_ref_arg<'db>(arenas: &Arenas<'db>, func_call: &ExprFunctionCall<'db>) -> bool { 140 | func_call.args.iter().any(|arg| match arg { 141 | ExprFunctionCallArg::Reference(_) => true, 142 | ExprFunctionCallArg::Value(expr_id) => match &arenas.exprs[*expr_id] { 143 | Expr::FunctionCall(expr_func) => ensure_no_ref_arg(arenas, expr_func), 144 | _ => false, 145 | }, 146 | }) 147 | } 148 | -------------------------------------------------------------------------------- /src/lints/manual/manual_expect_err.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::ids::ModuleItemId; 2 | use cairo_lang_defs::plugin::PluginDiagnostic; 3 | use cairo_lang_diagnostics::Severity; 4 | use cairo_lang_syntax::node::{ 5 | SyntaxNode, TypedStablePtr, TypedSyntaxNode, 6 | ast::{ExprIf, ExprMatch}, 7 | kind::SyntaxKind, 8 | }; 9 | 10 | use crate::{ 11 | context::CairoLintKind, 12 | fixer::InternalFix, 13 | queries::{get_all_function_bodies, get_all_if_expressions, get_all_match_expressions}, 14 | }; 15 | use crate::{ 16 | context::Lint, 17 | lints::manual::{ 18 | ManualLint, check_manual, check_manual_if, 19 | helpers::{expr_if_get_var_name_and_err, expr_match_get_var_name_and_err}, 20 | }, 21 | }; 22 | use salsa::Database; 23 | 24 | pub struct ManualExpectErr; 25 | 26 | /// ## What it does 27 | /// 28 | /// Checks for manual implementation of `expect_err` method in match and if expressions. 29 | /// 30 | /// ## Example 31 | /// 32 | /// ```cairo 33 | /// fn main() { 34 | /// let foo: Result = Result::Err('err'); 35 | /// let err = 'this is an err'; 36 | /// let _foo = match foo { 37 | /// Result::Ok(_) => core::panic_with_felt252(err), 38 | /// Result::Err(x) => x, 39 | /// }; 40 | /// } 41 | /// ``` 42 | /// 43 | /// Can be rewritten as: 44 | /// 45 | /// ```cairo 46 | /// fn main() { 47 | /// let foo: Result = Result::Err('err'); 48 | /// let err = 'this is an err'; 49 | /// let _foo = foo.expect_err(err); 50 | /// } 51 | /// ``` 52 | impl Lint for ManualExpectErr { 53 | fn allowed_name(&self) -> &'static str { 54 | "manual_expect_err" 55 | } 56 | 57 | fn diagnostic_message(&self) -> &'static str { 58 | "Manual match for `expect_err` detected. Consider using `expect_err()` instead" 59 | } 60 | 61 | fn kind(&self) -> CairoLintKind { 62 | CairoLintKind::ManualExpectErr 63 | } 64 | 65 | fn has_fixer(&self) -> bool { 66 | true 67 | } 68 | 69 | fn fix<'db>(&self, db: &'db dyn Database, node: SyntaxNode<'db>) -> Option> { 70 | fix_manual_expect_err(db, node) 71 | } 72 | 73 | fn fix_message(&self) -> Option<&'static str> { 74 | Some("Replace manual `expect_err` with `expect_err()` method") 75 | } 76 | } 77 | 78 | #[tracing::instrument(skip_all, level = "trace")] 79 | pub fn check_manual_expect_err<'db>( 80 | db: &'db dyn Database, 81 | item: &ModuleItemId<'db>, 82 | diagnostics: &mut Vec>, 83 | ) { 84 | let function_bodies = get_all_function_bodies(db, item); 85 | for function_body in function_bodies.iter() { 86 | let if_exprs = get_all_if_expressions(function_body); 87 | let match_exprs = get_all_match_expressions(function_body); 88 | let arenas = &function_body.arenas; 89 | for match_expr in match_exprs.iter() { 90 | if check_manual(db, match_expr, arenas, ManualLint::ManualExpectErr) { 91 | diagnostics.push(PluginDiagnostic { 92 | stable_ptr: match_expr.stable_ptr.untyped(), 93 | message: ManualExpectErr.diagnostic_message().to_owned(), 94 | severity: Severity::Warning, 95 | inner_span: None, 96 | }); 97 | } 98 | } 99 | for if_expr in if_exprs.iter() { 100 | if check_manual_if(db, if_expr, arenas, ManualLint::ManualExpectErr) { 101 | diagnostics.push(PluginDiagnostic { 102 | stable_ptr: if_expr.stable_ptr.untyped(), 103 | message: ManualExpectErr.diagnostic_message().to_owned(), 104 | severity: Severity::Warning, 105 | inner_span: None, 106 | }); 107 | } 108 | } 109 | } 110 | } 111 | 112 | /// Rewrites a manual implementation of expect err 113 | #[tracing::instrument(skip_all, level = "trace")] 114 | pub fn fix_manual_expect_err<'db>( 115 | db: &'db dyn Database, 116 | node: SyntaxNode<'db>, 117 | ) -> Option> { 118 | let fix = match node.kind(db) { 119 | SyntaxKind::ExprMatch => { 120 | let expr_match = ExprMatch::from_syntax_node(db, node); 121 | 122 | let (option_var_name, none_arm_err) = 123 | expr_match_get_var_name_and_err(expr_match, db, 0); 124 | 125 | format!("{}.expect_err({none_arm_err})", option_var_name.trim_end()) 126 | } 127 | SyntaxKind::ExprIf => { 128 | let expr_if = ExprIf::from_syntax_node(db, node); 129 | 130 | let (option_var_name, err) = expr_if_get_var_name_and_err(expr_if, db); 131 | 132 | format!("{}.expect_err({err})", option_var_name.trim_end()) 133 | } 134 | _ => panic!("SyntaxKind should be either ExprIf or ExprMatch"), 135 | }; 136 | Some(InternalFix { 137 | node, 138 | suggestion: fix, 139 | description: ManualExpectErr.fix_message().unwrap().to_string(), 140 | import_addition_paths: None, 141 | }) 142 | } 143 | -------------------------------------------------------------------------------- /tests/bitwise_for_parity_check/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{test_lint_diagnostics, test_lint_fixer}; 2 | 3 | const WITH_SINGLE_VARIABLE: &str = r#" 4 | fn main() { 5 | let _a = 200_u32 & 1; 6 | } 7 | "#; 8 | 9 | const WITH_MULTIPLE_VARIABLES: &str = r#" 10 | fn main() { 11 | let x = 150_u32; 12 | let y = 47; 13 | let _result = (x & 1) + (y & 1); 14 | } 15 | "#; 16 | 17 | const WITH_MULTIPLE_VARIABLES_ALLOWED: &str = r#" 18 | fn main() { 19 | let x = 150_u32; 20 | let y = 47; 21 | #[allow(bitwise_for_parity_check)] 22 | let _result = (x & 1) + (y & 1); 23 | } 24 | "#; 25 | 26 | const IN_A_LOOP: &str = r#" 27 | fn main() { 28 | for i in 0..10_u8 { 29 | let y = i & 1; 30 | println!("{}", y); 31 | } 32 | } 33 | "#; 34 | 35 | const WITH_CONDITIONAL_LOGIC: &str = r#" 36 | fn main() { 37 | let x = 17_u32; 38 | if (x & 1) == 1 { 39 | println!("Odd number"); 40 | } else { 41 | println!("Even number"); 42 | } 43 | } 44 | "#; 45 | 46 | const WITH_CONDITIONAL_LOGIC_ALLOWED: &str = r#" 47 | fn main() { 48 | let x = 17_u32; 49 | #[allow(bitwise_for_parity_check)] 50 | if (x & 1) == 1 { 51 | println!("Odd number"); 52 | } else { 53 | println!("Even number"); 54 | } 55 | } 56 | "#; 57 | 58 | #[test] 59 | fn with_single_variable_diagnostics() { 60 | test_lint_diagnostics!(WITH_SINGLE_VARIABLE, @r" 61 | Plugin diagnostic: You seem to be trying to use `&` for parity check. Consider using `DivRem::div_rem()` instead. 62 | --> lib.cairo:3:14 63 | let _a = 200_u32 & 1; 64 | ^^^^^^^^^^^ 65 | "); 66 | } 67 | 68 | #[test] 69 | fn with_single_variable_fixer() { 70 | test_lint_fixer!(WITH_SINGLE_VARIABLE, @r#" 71 | fn main() { 72 | let _a = 200_u32 & 1; 73 | } 74 | "#); 75 | } 76 | 77 | #[test] 78 | fn with_multiple_variables_diagnostics() { 79 | test_lint_diagnostics!(WITH_MULTIPLE_VARIABLES, @r" 80 | Plugin diagnostic: You seem to be trying to use `&` for parity check. Consider using `DivRem::div_rem()` instead. 81 | --> lib.cairo:5:20 82 | let _result = (x & 1) + (y & 1); 83 | ^^^^^ 84 | Plugin diagnostic: You seem to be trying to use `&` for parity check. Consider using `DivRem::div_rem()` instead. 85 | --> lib.cairo:5:30 86 | let _result = (x & 1) + (y & 1); 87 | ^^^^^ 88 | "); 89 | } 90 | 91 | #[test] 92 | fn with_multiple_variables_fixer() { 93 | test_lint_fixer!(WITH_MULTIPLE_VARIABLES, @r#" 94 | fn main() { 95 | let x = 150_u32; 96 | let y = 47; 97 | let _result = (x & 1) + (y & 1); 98 | } 99 | "#); 100 | } 101 | 102 | #[test] 103 | fn with_multiple_variables_allowed_diagnostics() { 104 | test_lint_diagnostics!(WITH_MULTIPLE_VARIABLES_ALLOWED, @r#" 105 | "#); 106 | } 107 | 108 | #[test] 109 | fn with_multiple_variables_allowed_fixer() { 110 | test_lint_fixer!(WITH_MULTIPLE_VARIABLES_ALLOWED, @r#" 111 | fn main() { 112 | let x = 150_u32; 113 | let y = 47; 114 | #[allow(bitwise_for_parity_check)] 115 | let _result = (x & 1) + (y & 1); 116 | } 117 | "#); 118 | } 119 | 120 | #[test] 121 | fn in_a_loop_diagnostics() { 122 | test_lint_diagnostics!(IN_A_LOOP, @r" 123 | Plugin diagnostic: You seem to be trying to use `&` for parity check. Consider using `DivRem::div_rem()` instead. 124 | --> lib.cairo:4:17 125 | let y = i & 1; 126 | ^^^^^ 127 | "); 128 | } 129 | 130 | #[test] 131 | fn in_a_loop_fixer() { 132 | test_lint_fixer!(IN_A_LOOP, @r#" 133 | fn main() { 134 | for i in 0..10_u8 { 135 | let y = i & 1; 136 | println!("{}", y); 137 | } 138 | } 139 | "#); 140 | } 141 | 142 | #[test] 143 | fn with_conditional_logic_diagnostics() { 144 | test_lint_diagnostics!(WITH_CONDITIONAL_LOGIC, @r" 145 | Plugin diagnostic: You seem to be trying to use `&` for parity check. Consider using `DivRem::div_rem()` instead. 146 | --> lib.cairo:4:9 147 | if (x & 1) == 1 { 148 | ^^^^^ 149 | "); 150 | } 151 | 152 | #[test] 153 | fn with_conditional_logic_fixer() { 154 | test_lint_fixer!(WITH_CONDITIONAL_LOGIC, @r#" 155 | fn main() { 156 | let x = 17_u32; 157 | if (x & 1) == 1 { 158 | println!("Odd number"); 159 | } else { 160 | println!("Even number"); 161 | } 162 | } 163 | "#); 164 | } 165 | 166 | #[test] 167 | fn with_conditional_logic_allowed_diagnostics() { 168 | test_lint_diagnostics!(WITH_CONDITIONAL_LOGIC_ALLOWED, @r#" 169 | "#); 170 | } 171 | 172 | #[test] 173 | fn with_conditional_logic_allowed_fixer() { 174 | test_lint_fixer!(WITH_CONDITIONAL_LOGIC_ALLOWED, @r#" 175 | fn main() { 176 | let x = 17_u32; 177 | #[allow(bitwise_for_parity_check)] 178 | if (x & 1) == 1 { 179 | println!("Odd number"); 180 | } else { 181 | println!("Even number"); 182 | } 183 | } 184 | "#); 185 | } 186 | -------------------------------------------------------------------------------- /tests/erasing_operations/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{test_lint_diagnostics, test_lint_fixer}; 2 | 3 | const MULTIPLICATION_BY_ZERO: &str = r#" 4 | fn main() { 5 | let x = 1; 6 | let _y = 0 * x; 7 | let _z = x * 0; 8 | } 9 | "#; 10 | 11 | const DIVISION_BY_ZERO: &str = r#" 12 | fn main() { 13 | let x = 1_u32; 14 | let _y = 0 / x; 15 | } 16 | "#; 17 | 18 | const DIVISION_BY_ZERO_ALLOWED: &str = r#" 19 | fn main() { 20 | let x = 1_u32; 21 | #[allow(erasing_op)] 22 | let _y = 0 / x; 23 | } 24 | "#; 25 | 26 | const BITWISE_AND_WITH_ZERO: &str = r#" 27 | fn main() { 28 | let x = 1_u32; 29 | let _y = x & 0; 30 | let _z = 0 & x; 31 | } 32 | "#; 33 | 34 | const MULTIPLE_OPERATIONS: &str = r#" 35 | fn main() { 36 | let x = 1_u32; 37 | let y = 5_u32; 38 | let z = 10_u32; 39 | let _f = ((x + y) * 0) & (z / 2); 40 | } 41 | "#; 42 | 43 | const MULTIPLE_BITWISE_OPERATIONS: &str = r#" 44 | fn main() { 45 | let x = 1_u32; 46 | let y = 5_u32; 47 | let z = 10_u32; 48 | let _result1 = (x * y + z) & (z & 0) ^ (z - y); 49 | } 50 | "#; 51 | 52 | #[test] 53 | fn multiplication_by_zero_diagnostics() { 54 | test_lint_diagnostics!(MULTIPLICATION_BY_ZERO, @r" 55 | Plugin diagnostic: This operation results in the value being erased (e.g., multiplication by 0). Consider replacing the entire expression with 0. 56 | --> lib.cairo:4:14 57 | let _y = 0 * x; 58 | ^^^^^ 59 | Plugin diagnostic: This operation results in the value being erased (e.g., multiplication by 0). Consider replacing the entire expression with 0. 60 | --> lib.cairo:5:14 61 | let _z = x * 0; 62 | ^^^^^ 63 | "); 64 | } 65 | 66 | #[test] 67 | fn multiplication_by_zero_fixer() { 68 | test_lint_fixer!(MULTIPLICATION_BY_ZERO, @r#" 69 | fn main() { 70 | let x = 1; 71 | let _y = 0 * x; 72 | let _z = x * 0; 73 | } 74 | "#); 75 | } 76 | 77 | #[test] 78 | fn division_by_zero_diagnostics() { 79 | test_lint_diagnostics!(DIVISION_BY_ZERO, @r" 80 | Plugin diagnostic: This operation results in the value being erased (e.g., multiplication by 0). Consider replacing the entire expression with 0. 81 | --> lib.cairo:4:14 82 | let _y = 0 / x; 83 | ^^^^^ 84 | "); 85 | } 86 | 87 | #[test] 88 | fn division_by_zero_fixer() { 89 | test_lint_fixer!(DIVISION_BY_ZERO, @r#" 90 | fn main() { 91 | let x = 1_u32; 92 | let _y = 0 / x; 93 | } 94 | "#); 95 | } 96 | 97 | #[test] 98 | fn division_by_zero_allowed_diagnostics() { 99 | test_lint_diagnostics!(DIVISION_BY_ZERO_ALLOWED, @r#" 100 | "#); 101 | } 102 | 103 | #[test] 104 | fn division_by_zero_allowed_fixer() { 105 | test_lint_fixer!(DIVISION_BY_ZERO_ALLOWED, @r#" 106 | fn main() { 107 | let x = 1_u32; 108 | #[allow(erasing_op)] 109 | let _y = 0 / x; 110 | } 111 | "#); 112 | } 113 | 114 | #[test] 115 | fn bitwise_and_with_zero_diagnostics() { 116 | test_lint_diagnostics!(BITWISE_AND_WITH_ZERO, @r" 117 | Plugin diagnostic: This operation results in the value being erased (e.g., multiplication by 0). Consider replacing the entire expression with 0. 118 | --> lib.cairo:4:14 119 | let _y = x & 0; 120 | ^^^^^ 121 | Plugin diagnostic: This operation results in the value being erased (e.g., multiplication by 0). Consider replacing the entire expression with 0. 122 | --> lib.cairo:5:14 123 | let _z = 0 & x; 124 | ^^^^^ 125 | "); 126 | } 127 | 128 | #[test] 129 | fn bitwise_and_with_zero_fixer() { 130 | test_lint_fixer!(BITWISE_AND_WITH_ZERO, @r#" 131 | fn main() { 132 | let x = 1_u32; 133 | let _y = x & 0; 134 | let _z = 0 & x; 135 | } 136 | "#); 137 | } 138 | 139 | #[test] 140 | fn multiple_operations_diagnostics() { 141 | test_lint_diagnostics!(MULTIPLE_OPERATIONS, @r" 142 | Plugin diagnostic: This operation results in the value being erased (e.g., multiplication by 0). Consider replacing the entire expression with 0. 143 | --> lib.cairo:6:15 144 | let _f = ((x + y) * 0) & (z / 2); 145 | ^^^^^^^^^^^ 146 | "); 147 | } 148 | 149 | #[test] 150 | fn multiple_operations_fixer() { 151 | test_lint_fixer!(MULTIPLE_OPERATIONS, @r" 152 | fn main() { 153 | let x = 1_u32; 154 | let y = 5_u32; 155 | let z = 10_u32; 156 | let _f = ((x + y) * 0) & (z / 2); 157 | } 158 | "); 159 | } 160 | 161 | #[test] 162 | fn multiple_bitwise_operations_diagnostics() { 163 | test_lint_diagnostics!(MULTIPLE_BITWISE_OPERATIONS, @r" 164 | Plugin diagnostic: This operation results in the value being erased (e.g., multiplication by 0). Consider replacing the entire expression with 0. 165 | --> lib.cairo:6:35 166 | let _result1 = (x * y + z) & (z & 0) ^ (z - y); 167 | ^^^^^ 168 | "); 169 | } 170 | 171 | #[test] 172 | fn multiple_bitwise_operations_fixer() { 173 | test_lint_fixer!(MULTIPLE_BITWISE_OPERATIONS, @r#" 174 | fn main() { 175 | let x = 1_u32; 176 | let y = 5_u32; 177 | let z = 10_u32; 178 | let _result1 = (x * y + z) & (z & 0) ^ (z - y); 179 | } 180 | "#); 181 | } 182 | -------------------------------------------------------------------------------- /tests/eq_op/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{test_lint_diagnostics, test_lint_fixer}; 2 | 3 | const SIMPLE_EQ_OP: &str = r#" 4 | fn foo(a: u256) -> bool { 5 | a == a 6 | } 7 | "#; 8 | 9 | const SIMPLE_NEQ_OP: &str = r#" 10 | fn foo(a: u256) -> bool { 11 | a != a 12 | } 13 | "#; 14 | 15 | const SIMPLE_LT_OP: &str = r#" 16 | fn foo(a: u256) -> bool { 17 | a < a 18 | } 19 | "#; 20 | 21 | const SIMPLE_GT_OP: &str = r#" 22 | fn foo(a: u256) -> bool { 23 | a > a 24 | } 25 | "#; 26 | 27 | const SIMPLE_BITWISE_OP: &str = r#" 28 | fn foo(a: u256) -> u256 { 29 | a & a 30 | } 31 | "#; 32 | 33 | const SIMPLE_BITWISE_OP_ALLOWED: &str = r#" 34 | fn foo(a: u256) -> u256 { 35 | #[allow(eq_logical_op)] 36 | a & a 37 | } 38 | "#; 39 | 40 | const SIMPLE_SUB_OP: &str = r#" 41 | fn foo(a: u256) -> u256 { 42 | a - a 43 | } 44 | "#; 45 | 46 | const SIMPLE_DIVIDE_OP: &str = r#" 47 | fn foo(a: u256) -> u256 { 48 | a / a 49 | } 50 | "#; 51 | 52 | const OP_WITH_METHOD_CALL: &str = r#" 53 | fn foo(a: Array) -> bool { 54 | a.len() == a.len() 55 | } 56 | "#; 57 | 58 | #[test] 59 | fn simple_eq_op_diagnostics() { 60 | test_lint_diagnostics!(SIMPLE_EQ_OP, @r" 61 | Plugin diagnostic: Comparison with identical operands, this operation always results in true and may indicate a logic error 62 | --> lib.cairo:3:5 63 | a == a 64 | ^^^^^^ 65 | "); 66 | } 67 | 68 | #[test] 69 | fn simple_eq_op_fixer() { 70 | test_lint_fixer!(SIMPLE_EQ_OP, @r#" 71 | fn foo(a: u256) -> bool { 72 | a == a 73 | } 74 | "#); 75 | } 76 | 77 | #[test] 78 | fn simple_neq_op_diagnostics() { 79 | test_lint_diagnostics!(SIMPLE_NEQ_OP, @r" 80 | Plugin diagnostic: Comparison with identical operands, this operation always results in false and may indicate a logic error 81 | --> lib.cairo:3:5 82 | a != a 83 | ^^^^^^ 84 | "); 85 | } 86 | 87 | #[test] 88 | fn simple_neq_op_fixer() { 89 | test_lint_fixer!(SIMPLE_NEQ_OP, @r#" 90 | fn foo(a: u256) -> bool { 91 | a != a 92 | } 93 | "#); 94 | } 95 | 96 | #[test] 97 | fn simple_lt_op_diagnostics() { 98 | test_lint_diagnostics!(SIMPLE_LT_OP, @r" 99 | Plugin diagnostic: Comparison with identical operands, this operation always results in false and may indicate a logic error 100 | --> lib.cairo:3:5 101 | a < a 102 | ^^^^^ 103 | "); 104 | } 105 | 106 | #[test] 107 | fn simple_lt_op_fixer() { 108 | test_lint_fixer!(SIMPLE_LT_OP, @r#" 109 | fn foo(a: u256) -> bool { 110 | a < a 111 | } 112 | "#); 113 | } 114 | 115 | #[test] 116 | fn simple_gt_op_diagnostics() { 117 | test_lint_diagnostics!(SIMPLE_GT_OP, @r" 118 | Plugin diagnostic: Comparison with identical operands, this operation always results in false and may indicate a logic error 119 | --> lib.cairo:3:5 120 | a > a 121 | ^^^^^ 122 | "); 123 | } 124 | 125 | #[test] 126 | fn simple_gt_op_fixer() { 127 | test_lint_fixer!(SIMPLE_GT_OP, @r#" 128 | fn foo(a: u256) -> bool { 129 | a > a 130 | } 131 | "#); 132 | } 133 | 134 | #[test] 135 | fn simple_bitwise_op_diagnostics() { 136 | test_lint_diagnostics!(SIMPLE_BITWISE_OP, @r" 137 | Plugin diagnostic: Logical operation with identical operands, this operation always results in the same value and may indicate a logic error 138 | --> lib.cairo:3:5 139 | a & a 140 | ^^^^^ 141 | "); 142 | } 143 | 144 | #[test] 145 | fn simple_bitwise_op_fixer() { 146 | test_lint_fixer!(SIMPLE_BITWISE_OP, @r#" 147 | fn foo(a: u256) -> u256 { 148 | a & a 149 | } 150 | "#); 151 | } 152 | 153 | #[test] 154 | fn simple_bitwise_op_allowed_diagnostics() { 155 | test_lint_diagnostics!(SIMPLE_BITWISE_OP_ALLOWED, @r#" 156 | "#); 157 | } 158 | 159 | #[test] 160 | fn simple_bitwise_op_allowed_fixer() { 161 | test_lint_fixer!(SIMPLE_BITWISE_OP_ALLOWED, @r#" 162 | fn foo(a: u256) -> u256 { 163 | #[allow(eq_logical_op)] 164 | a & a 165 | } 166 | "#); 167 | } 168 | 169 | #[test] 170 | fn simple_sub_op_diagnostics() { 171 | test_lint_diagnostics!(SIMPLE_SUB_OP, @r" 172 | Plugin diagnostic: Subtraction with identical operands, this operation always results in zero and may indicate a logic error 173 | --> lib.cairo:3:5 174 | a - a 175 | ^^^^^ 176 | "); 177 | } 178 | 179 | #[test] 180 | fn simple_sub_op_fixer() { 181 | test_lint_fixer!(SIMPLE_SUB_OP, @r#" 182 | fn foo(a: u256) -> u256 { 183 | a - a 184 | } 185 | "#); 186 | } 187 | 188 | #[test] 189 | fn simple_divide_op_diagnostics() { 190 | test_lint_diagnostics!(SIMPLE_DIVIDE_OP, @r" 191 | Plugin diagnostic: Division with identical operands, this operation always results in one (except for zero) and may indicate a logic error 192 | --> lib.cairo:3:5 193 | a / a 194 | ^^^^^ 195 | "); 196 | } 197 | 198 | #[test] 199 | fn simple_divide_op_fixer() { 200 | test_lint_fixer!(SIMPLE_DIVIDE_OP, @r#" 201 | fn foo(a: u256) -> u256 { 202 | a / a 203 | } 204 | "#); 205 | } 206 | 207 | #[test] 208 | fn op_with_method_call_diagnostics() { 209 | test_lint_diagnostics!(OP_WITH_METHOD_CALL, @r#" 210 | "#); 211 | } 212 | 213 | #[test] 214 | fn op_with_method_call_fixer() { 215 | test_lint_fixer!(OP_WITH_METHOD_CALL, @r#" 216 | fn foo(a: Array) -> bool { 217 | a.len() == a.len() 218 | } 219 | "#); 220 | } 221 | -------------------------------------------------------------------------------- /src/lints/panic.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::diagnostic_utils::StableLocation; 2 | use cairo_lang_defs::ids::ModuleItemId; 3 | use cairo_lang_defs::plugin::PluginDiagnostic; 4 | use cairo_lang_diagnostics::Severity; 5 | use cairo_lang_filesystem::db::get_originating_location; 6 | use cairo_lang_parser::db::ParserGroup; 7 | use cairo_lang_semantic::ExprFunctionCall; 8 | use cairo_lang_semantic::items::functions::GenericFunctionId; 9 | use cairo_lang_syntax::node::{TypedStablePtr, TypedSyntaxNode}; 10 | use if_chain::if_chain; 11 | use itertools::Itertools; 12 | 13 | use crate::context::{CairoLintKind, Lint}; 14 | 15 | use crate::LinterGroup; 16 | use crate::helper::ASSERT_FORMATTER_NAME; 17 | use crate::queries::{get_all_function_bodies, get_all_function_calls}; 18 | use cairo_lang_filesystem::ids::SpanInFile; 19 | use salsa::Database; 20 | 21 | pub struct PanicInCode; 22 | 23 | /// ## What it does 24 | /// 25 | /// Checks for panic usages. 26 | /// 27 | /// ## Example 28 | /// 29 | /// ```cairo 30 | /// fn main() { 31 | /// panic!("panic"); 32 | /// } 33 | /// ``` 34 | impl Lint for PanicInCode { 35 | fn allowed_name(&self) -> &'static str { 36 | "panic" 37 | } 38 | 39 | fn diagnostic_message(&self) -> &'static str { 40 | "Leaving `panic` in the code is discouraged." 41 | } 42 | 43 | fn kind(&self) -> CairoLintKind { 44 | CairoLintKind::Panic 45 | } 46 | 47 | fn is_enabled(&self) -> bool { 48 | false 49 | } 50 | } 51 | 52 | #[tracing::instrument(skip_all, level = "trace")] 53 | pub fn check_panic_usage<'db>( 54 | db: &'db dyn Database, 55 | item: &ModuleItemId<'db>, 56 | diagnostics: &mut Vec>, 57 | ) { 58 | let function_bodies = get_all_function_bodies(db, item); 59 | for function_body in function_bodies.iter() { 60 | let function_call_exprs = get_all_function_calls(function_body); 61 | for function_call_expr in function_call_exprs.unique() { 62 | check_single_panic_usage(db, &function_call_expr, diagnostics); 63 | } 64 | } 65 | } 66 | 67 | fn check_single_panic_usage<'db>( 68 | db: &'db dyn Database, 69 | function_call_expr: &ExprFunctionCall<'db>, 70 | diagnostics: &mut Vec>, 71 | ) { 72 | let init_node = function_call_expr.stable_ptr.lookup(db).as_syntax_node(); 73 | 74 | let concrete_function_id = function_call_expr 75 | .function 76 | .get_concrete(db) 77 | .generic_function; 78 | 79 | let corelib_context = db.corelib_context(); 80 | 81 | // If the function is the panic function from the corelib. 82 | let is_panic = if let GenericFunctionId::Extern(id) = concrete_function_id 83 | && id == corelib_context.get_panic_function_id() 84 | { 85 | true 86 | } else { 87 | false 88 | }; 89 | 90 | // If the function is the panic_with_byte_array function from the corelib. 91 | let is_panic_with_byte_array = if let GenericFunctionId::Free(id) = concrete_function_id 92 | && id == corelib_context.get_panic_with_byte_array_function_id() 93 | { 94 | true 95 | } else { 96 | false 97 | }; 98 | 99 | // We check if the panic comes from the `assert!` macro. 100 | let is_assert_panic = is_panic_with_byte_array 101 | && function_call_expr 102 | .stable_ptr 103 | .lookup(db) 104 | .as_syntax_node() 105 | .get_text(db) 106 | .contains(ASSERT_FORMATTER_NAME); 107 | 108 | // We skip non panic calls, and panic calls in assert macros. 109 | if !(is_panic || is_panic_with_byte_array) || is_assert_panic { 110 | return; 111 | } 112 | 113 | // Get the origination location of this panic as there is a `panic!` macro that generates virtual 114 | // files 115 | let initial_file_id = StableLocation::new(function_call_expr.stable_ptr.untyped()).file_id(db); 116 | let SpanInFile { file_id, span } = get_originating_location( 117 | db, 118 | SpanInFile { 119 | file_id: initial_file_id, 120 | span: function_call_expr 121 | .stable_ptr 122 | .lookup(db) 123 | .as_syntax_node() 124 | .span(db), 125 | }, 126 | None, 127 | ); 128 | // If the panic comes from a real file (macros generate code in new virtual files) 129 | if initial_file_id == file_id { 130 | diagnostics.push(PluginDiagnostic { 131 | stable_ptr: init_node.stable_ptr(db), 132 | message: PanicInCode.diagnostic_message().to_owned(), 133 | severity: Severity::Warning, 134 | inner_span: None, 135 | }); 136 | } else { 137 | // If the originating location is a different file get the syntax node that generated the 138 | // code that contains a panic. 139 | if_chain! { 140 | if let Some(text_position) = span.position_in_file(db, file_id); 141 | if let Ok(file_node) = db.file_syntax(file_id); 142 | then { 143 | let syntax_node = file_node.lookup_position(db, text_position.start); 144 | diagnostics.push(PluginDiagnostic { 145 | stable_ptr: syntax_node.stable_ptr(db), 146 | message: PanicInCode.diagnostic_message().to_owned(), 147 | severity: Severity::Warning, 148 | inner_span: None 149 | }); 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/lints/ifs/equatable_if_let.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::ids::ModuleItemId; 2 | use cairo_lang_defs::plugin::PluginDiagnostic; 3 | use cairo_lang_diagnostics::Severity; 4 | use cairo_lang_semantic::{Arenas, Condition, Expr, ExprIf, Pattern, PatternId}; 5 | 6 | use cairo_lang_syntax::node::{ 7 | SyntaxNode, TypedStablePtr, TypedSyntaxNode, 8 | ast::{Condition as AstCondition, ExprIf as AstExprIf}, 9 | }; 10 | 11 | use crate::context::{CairoLintKind, Lint}; 12 | 13 | use crate::fixer::InternalFix; 14 | use crate::queries::{get_all_function_bodies, get_all_if_expressions}; 15 | use salsa::Database; 16 | 17 | pub struct EquatableIfLet; 18 | 19 | /// ## What it does 20 | /// 21 | /// Checks for `if let` pattern matching that can be replaced by a simple comparison. 22 | /// 23 | /// ## Example 24 | /// 25 | /// ```cairo 26 | /// if let Some(2) = a { 27 | /// // Code 28 | /// } 29 | /// ``` 30 | /// 31 | /// Could be replaced by 32 | /// 33 | /// ```cairo 34 | /// if a == Some(2) { 35 | /// // Code 36 | /// } 37 | /// ``` 38 | impl Lint for EquatableIfLet { 39 | fn allowed_name(&self) -> &'static str { 40 | "equatable_if_let" 41 | } 42 | 43 | fn diagnostic_message(&self) -> &'static str { 44 | "`if let` pattern used for equatable value. Consider using a simple comparison `==` instead" 45 | } 46 | 47 | fn kind(&self) -> CairoLintKind { 48 | CairoLintKind::EquatableIfLet 49 | } 50 | 51 | fn has_fixer(&self) -> bool { 52 | true 53 | } 54 | 55 | fn fix<'db>(&self, db: &'db dyn Database, node: SyntaxNode<'db>) -> Option> { 56 | fix_equatable_if_let(db, node) 57 | } 58 | 59 | fn fix_message(&self) -> Option<&'static str> { 60 | Some("Replace `if let` with `==` comparison") 61 | } 62 | } 63 | 64 | #[tracing::instrument(skip_all, level = "trace")] 65 | pub fn check_equatable_if_let<'db>( 66 | db: &'db dyn Database, 67 | item: &ModuleItemId<'db>, 68 | diagnostics: &mut Vec>, 69 | ) { 70 | let function_bodies = get_all_function_bodies(db, item); 71 | for function_body in function_bodies.iter() { 72 | let if_exprs = get_all_if_expressions(function_body); 73 | let arenas = &function_body.arenas; 74 | for if_expr in if_exprs.iter() { 75 | check_single_equatable_if_let(db, if_expr, arenas, diagnostics); 76 | } 77 | } 78 | } 79 | 80 | fn check_single_equatable_if_let<'db>( 81 | _db: &'db dyn Database, 82 | if_expr: &ExprIf<'db>, 83 | arenas: &Arenas<'db>, 84 | diagnostics: &mut Vec>, 85 | ) { 86 | if let Some(Condition::Let(condition_let, patterns)) = &if_expr.conditions.first() { 87 | // Simple literals and variables 88 | let expr_is_simple = matches!( 89 | arenas.exprs[*condition_let], 90 | Expr::Literal(_) | Expr::StringLiteral(_) | Expr::Var(_) 91 | ); 92 | let condition_is_simple = is_simple_equality_condition(patterns, arenas); 93 | 94 | if expr_is_simple && condition_is_simple { 95 | diagnostics.push(PluginDiagnostic { 96 | stable_ptr: if_expr.stable_ptr.untyped(), 97 | message: EquatableIfLet.diagnostic_message().to_string(), 98 | severity: Severity::Warning, 99 | inner_span: None, 100 | }); 101 | } 102 | } 103 | } 104 | 105 | fn is_simple_equality_condition(patterns: &[PatternId], arenas: &Arenas) -> bool { 106 | for pattern in patterns { 107 | match &arenas.patterns[*pattern] { 108 | Pattern::Literal(_) | Pattern::StringLiteral(_) => return true, 109 | Pattern::EnumVariant(pat) => { 110 | return pat.inner_pattern.is_none_or(|pat_id| { 111 | matches!( 112 | arenas.patterns[pat_id], 113 | Pattern::Literal(_) | Pattern::StringLiteral(_) 114 | ) 115 | }); 116 | } 117 | _ => continue, 118 | } 119 | } 120 | false 121 | } 122 | 123 | /// Rewrites a useless `if let` to a simple `if` 124 | #[tracing::instrument(skip_all, level = "trace")] 125 | pub fn fix_equatable_if_let<'db>( 126 | db: &'db dyn Database, 127 | node: SyntaxNode<'db>, 128 | ) -> Option> { 129 | let expr = AstExprIf::from_syntax_node(db, node); 130 | let mut conditions = expr.conditions(db).elements(db); 131 | let condition = conditions.next()?; 132 | 133 | let fixed_condition = match condition { 134 | AstCondition::Let(condition_let) => { 135 | format!( 136 | "{} == {} ", 137 | condition_let 138 | .expr(db) 139 | .as_syntax_node() 140 | .get_text(db) 141 | .trim_end(), 142 | condition_let 143 | .patterns(db) 144 | .as_syntax_node() 145 | .get_text(db) 146 | .trim_end(), 147 | ) 148 | } 149 | _ => panic!("Incorrect diagnostic"), 150 | }; 151 | 152 | Some(InternalFix { 153 | node, 154 | suggestion: format!( 155 | "{}{}{}", 156 | expr.if_kw(db).as_syntax_node().get_text(db), 157 | fixed_condition, 158 | expr.if_block(db).as_syntax_node().get_text(db), 159 | ), 160 | description: EquatableIfLet.fix_message().unwrap().to_string(), 161 | import_addition_paths: None, 162 | }) 163 | } 164 | -------------------------------------------------------------------------------- /src/lints/manual/manual_expect.rs: -------------------------------------------------------------------------------- 1 | use cairo_lang_defs::ids::ModuleItemId; 2 | use cairo_lang_defs::plugin::PluginDiagnostic; 3 | use cairo_lang_diagnostics::Severity; 4 | 5 | use cairo_lang_syntax::node::kind::SyntaxKind; 6 | use cairo_lang_syntax::node::{ 7 | SyntaxNode, TypedStablePtr, TypedSyntaxNode, 8 | ast::{ExprIf, ExprMatch}, 9 | }; 10 | 11 | use crate::context::{CairoLintKind, Lint}; 12 | 13 | use crate::fixer::InternalFix; 14 | use crate::lints::manual::helpers::{ 15 | expr_if_get_var_name_and_err, expr_match_get_var_name_and_err, 16 | }; 17 | use crate::lints::manual::{ManualLint, check_manual, check_manual_if}; 18 | use crate::queries::{get_all_function_bodies, get_all_if_expressions, get_all_match_expressions}; 19 | use salsa::Database; 20 | 21 | pub struct ManualExpect; 22 | 23 | /// ## What it does 24 | /// 25 | /// Checks for manual implementations of `expect`. 26 | /// 27 | /// ## Example 28 | /// 29 | /// ```cairo 30 | /// fn main() { 31 | /// let foo: Option:: = Option::None; 32 | /// let _foo = match foo { 33 | /// Option::Some(x) => x, 34 | /// Option::None => core::panic_with_felt252('err'), 35 | /// }; 36 | /// } 37 | /// ``` 38 | /// 39 | /// Can be rewritten as: 40 | /// 41 | /// ```cairo 42 | /// fn main() { 43 | /// let foo: Option:: = Option::None; 44 | /// let _foo = foo.expect('err'); 45 | /// } 46 | /// ``` 47 | impl Lint for ManualExpect { 48 | fn allowed_name(&self) -> &'static str { 49 | "manual_expect" 50 | } 51 | 52 | fn diagnostic_message(&self) -> &'static str { 53 | "Manual match for expect detected. Consider using `expect()` instead" 54 | } 55 | 56 | fn kind(&self) -> CairoLintKind { 57 | CairoLintKind::ManualExpect 58 | } 59 | 60 | fn has_fixer(&self) -> bool { 61 | true 62 | } 63 | 64 | fn fix<'db>(&self, db: &'db dyn Database, node: SyntaxNode<'db>) -> Option> { 65 | fix_manual_expect(db, node) 66 | } 67 | 68 | fn fix_message(&self) -> Option<&'static str> { 69 | Some("Replace manual `expect` with `expect()` method") 70 | } 71 | } 72 | 73 | #[tracing::instrument(skip_all, level = "trace")] 74 | pub fn check_manual_expect<'db>( 75 | db: &'db dyn Database, 76 | item: &ModuleItemId<'db>, 77 | diagnostics: &mut Vec>, 78 | ) { 79 | let function_bodies = get_all_function_bodies(db, item); 80 | for function_body in function_bodies.iter() { 81 | let if_exprs = get_all_if_expressions(function_body); 82 | let match_exprs = get_all_match_expressions(function_body); 83 | let arenas = &function_body.arenas; 84 | for match_expr in match_exprs.iter() { 85 | if check_manual(db, match_expr, arenas, ManualLint::ManualOptExpect) { 86 | diagnostics.push(PluginDiagnostic { 87 | stable_ptr: match_expr.stable_ptr.untyped(), 88 | message: ManualExpect.diagnostic_message().to_owned(), 89 | severity: Severity::Warning, 90 | inner_span: None, 91 | }); 92 | } 93 | 94 | if check_manual(db, match_expr, arenas, ManualLint::ManualResExpect) { 95 | diagnostics.push(PluginDiagnostic { 96 | stable_ptr: match_expr.stable_ptr.untyped(), 97 | message: ManualExpect.diagnostic_message().to_owned(), 98 | severity: Severity::Warning, 99 | inner_span: None, 100 | }); 101 | } 102 | } 103 | for if_expr in if_exprs.iter() { 104 | if check_manual_if(db, if_expr, arenas, ManualLint::ManualOptExpect) { 105 | diagnostics.push(PluginDiagnostic { 106 | stable_ptr: if_expr.stable_ptr.untyped(), 107 | message: ManualExpect.diagnostic_message().to_owned(), 108 | severity: Severity::Warning, 109 | inner_span: None, 110 | }); 111 | } 112 | 113 | if check_manual_if(db, if_expr, arenas, ManualLint::ManualResExpect) { 114 | diagnostics.push(PluginDiagnostic { 115 | stable_ptr: if_expr.stable_ptr.untyped(), 116 | message: ManualExpect.diagnostic_message().to_owned(), 117 | severity: Severity::Warning, 118 | inner_span: None, 119 | }); 120 | } 121 | } 122 | } 123 | } 124 | 125 | /// Rewrites a manual implementation of expect 126 | #[tracing::instrument(skip_all, level = "trace")] 127 | pub fn fix_manual_expect<'db>( 128 | db: &'db dyn Database, 129 | node: SyntaxNode<'db>, 130 | ) -> Option> { 131 | let fix = match node.kind(db) { 132 | SyntaxKind::ExprMatch => { 133 | let expr_match = ExprMatch::from_syntax_node(db, node); 134 | 135 | let (option_var_name, none_arm_err) = 136 | expr_match_get_var_name_and_err(expr_match, db, 1); 137 | 138 | format!("{}.expect({none_arm_err})", option_var_name.trim_end()) 139 | } 140 | SyntaxKind::ExprIf => { 141 | let expr_if = ExprIf::from_syntax_node(db, node); 142 | 143 | let (option_var_name, err) = expr_if_get_var_name_and_err(expr_if, db); 144 | 145 | format!("{}.expect({err})", option_var_name.trim_end()) 146 | } 147 | _ => panic!("SyntaxKind should be either ExprIf or ExprMatch"), 148 | }; 149 | Some(InternalFix { 150 | node, 151 | suggestion: fix, 152 | description: ManualExpect.fix_message().unwrap().to_string(), 153 | import_addition_paths: None, 154 | }) 155 | } 156 | -------------------------------------------------------------------------------- /tests/unit_return_type/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{test_lint_diagnostics, test_lint_fixer}; 2 | 3 | const SIMPLE_RETURN_UNIT_TYPE: &str = r#" 4 | fn test_fn() -> () { 5 | () 6 | } 7 | 8 | fn main() { 9 | println!("Hello, world!"); 10 | test_fn() 11 | } 12 | "#; 13 | 14 | const SIMPLE_RETURN_UNIT_TYPE_WITH_IMPLICIT_UNIT_TYPE: &str = r#" 15 | fn test_fn() { 16 | () 17 | } 18 | 19 | fn main() { 20 | println!("Hello, world!"); 21 | test_fn() 22 | } 23 | "#; 24 | 25 | const SIMPLE_RETURN_UNIT_TYPE_ALLOWED: &str = r#" 26 | #[allow(unit_return_type)] 27 | fn test_fn() -> () { 28 | () 29 | } 30 | fn main() { 31 | println!("Hello, world!"); 32 | test_fn() 33 | } 34 | "#; 35 | 36 | const RETURN_UNIT_TYPE_IN_TRAIT: &str = r#" 37 | trait MyTrait { 38 | fn test_fn() -> (); 39 | } 40 | 41 | fn main() { 42 | println!("Hello, world!"); 43 | } 44 | "#; 45 | 46 | const RETURN_UNIT_TYPE_IN_TRAIT_ALLOWED: &str = r#" 47 | trait MyTrait { 48 | #[allow(unit_return_type)] 49 | fn test_fn() -> (); 50 | } 51 | 52 | fn main() { 53 | println!("Hello, world!"); 54 | } 55 | "#; 56 | 57 | const RETURN_UNIT_TYPE_IN_IMPL: &str = r#" 58 | trait MyTrait { 59 | fn test_fn(); 60 | } 61 | 62 | impl MyTraitImpl of MyTrait { 63 | fn test_fn() -> () { 64 | () 65 | } 66 | } 67 | 68 | fn main() { 69 | println!("Hello, world!"); 70 | } 71 | "#; 72 | 73 | const RETURN_UNIT_TYPE_IN_IMPL_ALLOWED: &str = r#" 74 | trait MyTrait { 75 | fn test_fn(); 76 | } 77 | 78 | impl MyTraitImpl of MyTrait { 79 | #[allow(unit_return_type)] 80 | fn test_fn() -> () { 81 | () 82 | } 83 | } 84 | 85 | fn main() { 86 | println!("Hello, world!"); 87 | } 88 | "#; 89 | 90 | #[test] 91 | fn simple_return_unit_type_diagnostics() { 92 | test_lint_diagnostics!(SIMPLE_RETURN_UNIT_TYPE, @r" 93 | Plugin diagnostic: unnecessary declared unit return type `()` 94 | --> lib.cairo:2:11 95 | fn test_fn() -> () { 96 | ^^^^^^^^ 97 | "); 98 | } 99 | 100 | #[test] 101 | fn simple_return_unit_type_fixer() { 102 | test_lint_fixer!(SIMPLE_RETURN_UNIT_TYPE, @r#" 103 | fn test_fn() { 104 | () 105 | } 106 | 107 | fn main() { 108 | println!("Hello, world!"); 109 | test_fn() 110 | } 111 | "#); 112 | } 113 | 114 | #[test] 115 | fn simple_return_unit_type_with_implicit_unit_type_diagnostics() { 116 | test_lint_diagnostics!(SIMPLE_RETURN_UNIT_TYPE_WITH_IMPLICIT_UNIT_TYPE, @r""); 117 | } 118 | 119 | #[test] 120 | fn simple_return_unit_type_with_implicit_unit_type_fixer() { 121 | test_lint_fixer!(SIMPLE_RETURN_UNIT_TYPE_WITH_IMPLICIT_UNIT_TYPE, @r#" 122 | fn test_fn() { 123 | () 124 | } 125 | 126 | fn main() { 127 | println!("Hello, world!"); 128 | test_fn() 129 | } 130 | "#); 131 | } 132 | 133 | #[test] 134 | fn simple_return_unit_type_allowed_diagnostics() { 135 | test_lint_diagnostics!(SIMPLE_RETURN_UNIT_TYPE_ALLOWED, @r#""#); 136 | } 137 | 138 | #[test] 139 | fn simple_return_unit_type_allowed_fixer() { 140 | test_lint_fixer!(SIMPLE_RETURN_UNIT_TYPE_ALLOWED, @r##" 141 | #[allow(unit_return_type)] 142 | fn test_fn() -> () { 143 | () 144 | } 145 | fn main() { 146 | println!("Hello, world!"); 147 | test_fn() 148 | } 149 | "##); 150 | } 151 | 152 | #[test] 153 | fn return_unit_type_in_trait_diagnostics() { 154 | test_lint_diagnostics!(RETURN_UNIT_TYPE_IN_TRAIT, @r" 155 | Plugin diagnostic: unnecessary declared unit return type `()` 156 | --> lib.cairo:3:15 157 | fn test_fn() -> (); 158 | ^^^^^^^^ 159 | "); 160 | } 161 | 162 | #[test] 163 | fn return_unit_type_in_trait_fixer() { 164 | test_lint_fixer!(RETURN_UNIT_TYPE_IN_TRAIT, @r#" 165 | trait MyTrait { 166 | fn test_fn(); 167 | } 168 | 169 | fn main() { 170 | println!("Hello, world!"); 171 | } 172 | "#); 173 | } 174 | 175 | #[test] 176 | fn return_unit_type_in_trait_allowed_diagnostics() { 177 | test_lint_diagnostics!(RETURN_UNIT_TYPE_IN_TRAIT_ALLOWED, @r#""#); 178 | } 179 | 180 | #[test] 181 | fn return_unit_type_in_trait_allowed_fixer() { 182 | test_lint_fixer!(RETURN_UNIT_TYPE_IN_TRAIT_ALLOWED, @r#" 183 | trait MyTrait { 184 | #[allow(unit_return_type)] 185 | fn test_fn() -> (); 186 | } 187 | 188 | fn main() { 189 | println!("Hello, world!"); 190 | } 191 | "#); 192 | } 193 | 194 | #[test] 195 | fn return_unit_type_in_impl_diagnostics() { 196 | test_lint_diagnostics!(RETURN_UNIT_TYPE_IN_IMPL, @r" 197 | Plugin diagnostic: unnecessary declared unit return type `()` 198 | --> lib.cairo:7:15 199 | fn test_fn() -> () { 200 | ^^^^^^^^ 201 | "); 202 | } 203 | 204 | #[test] 205 | fn return_unit_type_in_impl_fixer() { 206 | test_lint_fixer!(RETURN_UNIT_TYPE_IN_IMPL, @r#" 207 | trait MyTrait { 208 | fn test_fn(); 209 | } 210 | 211 | impl MyTraitImpl of MyTrait { 212 | fn test_fn() { 213 | () 214 | } 215 | } 216 | 217 | fn main() { 218 | println!("Hello, world!"); 219 | } 220 | "#); 221 | } 222 | 223 | #[test] 224 | fn return_unit_type_in_impl_allowed_diagnostics() { 225 | test_lint_diagnostics!(RETURN_UNIT_TYPE_IN_IMPL_ALLOWED, @""); 226 | } 227 | 228 | #[test] 229 | fn return_unit_type_in_impl_allowed_fixer() { 230 | test_lint_fixer!(RETURN_UNIT_TYPE_IN_IMPL_ALLOWED, @r#" 231 | trait MyTrait { 232 | fn test_fn(); 233 | } 234 | 235 | impl MyTraitImpl of MyTrait { 236 | #[allow(unit_return_type)] 237 | fn test_fn() -> () { 238 | () 239 | } 240 | } 241 | 242 | fn main() { 243 | println!("Hello, world!"); 244 | } 245 | "#); 246 | } 247 | --------------------------------------------------------------------------------