Skip to content

add new lint: ip_constant #14878

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open

add new lint: ip_constant #14878

wants to merge 11 commits into from

Conversation

relaxcn
Copy link
Contributor

@relaxcn relaxcn commented May 23, 2025

Closes: #14819

changelog: new lint: [ip_constant]

@rustbot
Copy link
Collaborator

rustbot commented May 23, 2025

r? @llogiq

rustbot has assigned @llogiq.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label May 23, 2025
Copy link
Contributor

@samueltardieu samueltardieu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why single out Ipv4Addr::LOCALHOST when several other constants exist? Why not also cover Ipv6Addr?

Also, please include tests in which either the Ipv4Addr::new or Ipv4Addr come from a macro, to ensure that the suggestion is properly emitted.

@relaxcn relaxcn changed the title add new lint: localhost_hardcode add new lint: ipv4v6_constant_hardcode May 24, 2025
@relaxcn
Copy link
Contributor Author

relaxcn commented May 24, 2025

Why single out Ipv4Addr::LOCALHOST when several other constants exist? Why not also cover Ipv6Addr?

Also, please include tests in which either the Ipv4Addr::new or Ipv4Addr come from a macro, to ensure that the suggestion is properly emitted.

Thanks for your comments.

It's a mistake, more constants are supported now.

For the second point you mentioned, I wrote some test cases for it, but because of the below code in methods/mod.rs:

impl<'tcx> LateLintPass<'tcx> for Methods {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
if expr.span.from_expansion() {
return;
}

The macro related codes can not make the suggestion be emitted.

macro_rules! ipv4_new {
    ($a:expr, $b:expr, $c:expr, $d:expr) => {
        std::net::Ipv4Addr::new($a, $b, $c, $d)
    };
}

fn macro_test() {
    let _ = ipv4_new!(127, 0, 0, 1);
    // no lint
    let _ = ipv4_new!(255, 255, 255, 255);
    // no lint
    let _ = ipv4_new!(0, 0, 0, 0);
    // no lint
}

I'm not sure it is acceptable or not.

@relaxcn
Copy link
Contributor Author

relaxcn commented May 24, 2025

Additional, I changed the lint name from localhost_hardcode to ipv4v6_constant_hardcode.

@Jarcho
Copy link
Contributor

Jarcho commented May 24, 2025

A name like manual_ipaddr_constants or inline_ipaddr_constants would be more accurate. Hard coded has the specific meaning of being part of the program rather than loaded by the program.

Copy link
Contributor

@llogiq llogiq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a good starting point. I'd like to see a more perf-aware implementation, and perhaps a test where the numbers are include!d from an external file (which might be considered a false positive).

@llogiq
Copy link
Contributor

llogiq commented May 24, 2025

Also re the name, we already have approx_constant, so by analogy ip_constant would be a good short lint name.

@relaxcn
Copy link
Contributor Author

relaxcn commented May 24, 2025

Also re the name, we already have approx_constant, so by analogy ip_constant would be a good short lint name.

Yes, agree.

@relaxcn
Copy link
Contributor Author

relaxcn commented May 24, 2025

This looks like a good starting point. I'd like to see a more perf-aware implementation, and perhaps a test where the numbers are include!d from an external file (which might be considered a false positive).

I wrote a test cases about include! macro, but I don't know how to skip the file that was included when running ui-test.

For example a file ip_constant_included.rs:

const EXTERNAL_CONST_U8_127: u8 = 127;

const EXTERNAL_CONST_U16_0: u16 = 0;

When I run cargo uitest, there will be a error:

FAILED TEST: tests/ui/ip_constant_included.rs
command: CLIPPY_CONF_DIR="tests" RUSTC_ICE="0" "/home/boot0x7c00/Documents/rust-clippy/target/debug/clippy-driver" "--error-format=json" "--emit=metadata" "-Aunused" "-Ainternal_features" "-Zui-testing" "-Zdeduplicate-diagnostics=no" "-Dwarnings" "-Ldependency=/home/boot0x7c00/Documents/rust-clippy/target/debug/deps" "--extern=clippy_config=/home/boot0x7c00/Documents/rust-clippy/target/debug/deps/libclippy_config-576e2250f00c1355.rlib" "--extern=clippy_lints=/home/boot0x7c00/Documents/rust-clippy/target/debug/deps/libclippy_lints-d7f6f9ef470f8304.rlib" "--extern=clippy_utils=/home/boot0x7c00/Documents/rust-clippy/target/debug/deps/libclippy_utils-266a230a089912ed.rlib" "--extern=futures=/home/boot0x7c00/Documents/rust-clippy/target/debug/deps/libfutures-aa4ff275b96de74b.rlib" "--extern=if_chain=/home/boot0x7c00/Documents/rust-clippy/target/debug/deps/libif_chain-29ce38907b177558.rlib" "--extern=itertools=/home/boot0x7c00/Documents/rust-clippy/target/debug/deps/libitertools-71dce43a067d1845.rlib" "--extern=parking_lot=/home/boot0x7c00/Documents/rust-clippy/target/debug/deps/libparking_lot-ff4ee963c30c9f76.rlib" "--extern=quote=/home/boot0x7c00/Documents/rust-clippy/target/debug/deps/libquote-a701a81c28ede506.rlib" "--extern=regex=/home/boot0x7c00/Documents/rust-clippy/target/debug/deps/libregex-1b6fbb71c3f53d74.rlib" "--extern=serde=/home/boot0x7c00/Documents/rust-clippy/target/debug/deps/libserde-12f093ef6003304f.rlib" "--extern=serde_derive=/home/boot0x7c00/.rustup/toolchains/nightly-2025-05-21-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libserde_derive-fe9a7505d3290aa4.so" "--extern=syn=/home/boot0x7c00/Documents/rust-clippy/target/debug/deps/libsyn-7c51c2fc387254a9.rlib" "--extern=tokio=/home/boot0x7c00/Documents/rust-clippy/target/debug/deps/libtokio-10d297750b944ab0.rlib" "--crate-type=lib" "--out-dir" "target/ui_test/0/tests/ui" "tests/ui/ip_constant_included.rs" "--edition" "2024"

error: expected error patterns, but found none

full stderr:

full stdout:


FAILURES:
    tests/ui/ip_constant_included.rs

test result: FAIL. 1 failed; 2 passed; 1087 filtered out

Do you know someway to skip testing this file?

It had been solved by adding //@only-target: NOWAY.

@relaxcn relaxcn changed the title add new lint: ipv4v6_constant_hardcode add new lint: ip_constant May 24, 2025
@llogiq
Copy link
Contributor

llogiq commented May 24, 2025

No, I meant including a localhost.txt file via the include!(..) macro.

@relaxcn
Copy link
Contributor Author

relaxcn commented May 24, 2025

No, I meant including a localhost.txt file via the include!(..) macro.

Sorry, do you mean this file localhost.txt?

std::net::Ipv4Addr::new(127, 0, 0, 1)

Then using it in ui-test file:

let _ = include!("localhost.txt");
// should not lint?

I have tried it, there is an error in localhost.txt file:

error: use `std::net::Ipv4Addr::LOCALHOST` instead
  --> tests/ui/localhost.txt:1:1
   |
LL | std::net::Ipv4Addr::new(127, 0, 0, 1)
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `std::net::Ipv4Addr::LOCALHOST`

It's quite surprising to me that the span of the expression can actually point to a specific external file.

I used to write static analysis tools mainly for C++ code. In C++, macros are just simple text replacements, and in the final AST, it's basically impossible to trace the replaced code back to the file or location it came from.

In fact, due to macro expansion, the positions in the code before and after preprocessing can even change.

@llogiq
Copy link
Contributor

llogiq commented May 24, 2025

How about let _ = std::net::Ipv4Addr::new(include!("localhost.txt"));?

@relaxcn
Copy link
Contributor Author

relaxcn commented May 25, 2025

How about let _ = std::net::Ipv4Addr::new(include!("localhost.txt"));?

I got an error:

error: include macro expected single expression in source
 --> tests/ui/localhost.txt:1:4
  |
1 | 127, 0, 0, 1
  |    ^
  |
  = note: `#[deny(incomplete_include)]` on by default

error[E0061]: this function takes 4 arguments but 1 argument was supplied
   --> tests/ui/ip_constant.rs:126:13
    |
126 |     let _ = std::net::Ipv4Addr::new(include!("localhost.txt"));
    |             ^^^^^^^^^^^^^^^^^^^^^^^--------------------------- three arguments of type `u8`, `u8`, and `u8` are missing
    |
note: associated function defined here
   --> /home/ciel/.rustup/toolchains/nightly-2025-05-21-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/net/ip_addr.rs:496:18
    |
496 |     pub const fn new(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr {
    |                  ^^^
help: provide the arguments
    |
126 -     let _ = std::net::Ipv4Addr::new(include!("localhost.txt"));
126 +     let _ = std::net::Ipv4Addr::new(127, /* u8 */, /* u8 */, /* u8 */);
    |

The localhost.txt is:

127, 0, 0, 1

The include! macro parses a file as an expression or an item according to the context, so I think this way is not working, the Ipv4Addr::new requires four arguments.

@relaxcn
Copy link
Contributor Author

relaxcn commented May 25, 2025

When I wrote test case like this:

//@error-in-other-file: use `std::net::Ipv4Addr::LOCALHOST` instead
fn external_constant_test() {
    let _ = include!("localhost.txt");
    // lint in external file `localhost.txt`
}

The localhost.txt:

std::net::Ipv4Addr::new(127, 0, 0, 1)

I will get an error when running TESTNAME="ip_constant.rs" cargo uitest -- --bless :

FAILED TEST: tests/ui/ip_constant.rs
command: rustfix tests/ui/ip_constant.rs

error: failed to apply suggestions for tests/ui/ip_constant.rs with rustfix
cannot apply suggestions for `tests/ui/localhost.txt` since main file is `tests/ui/ip_constant.rs`. Please use `//@no-rustfix` to disable rustfix
Add //@no-rustfix to the test file to ignore rustfix suggestions

So I separated the test cases that use include! macro.

@llogiq
Copy link
Contributor

llogiq commented May 25, 2025

Ah, interesting, I didn't know that. Good thing you've got that test down. However, in that case, the lint should not apply MachineApplicable to the suggestion. Isn't the output of the include! macro also marked as from_expansion?

@relaxcn
Copy link
Contributor Author

relaxcn commented May 25, 2025

Ah, interesting, I didn't know that. Good thing you've got that test down. However, in that case, the lint should not apply MachineApplicable to the suggestion. Isn't the output of the include! macro also marked as from_expansion?

Yes, you are right. That case should not apply auto-fix. But it's hard to handle include! macro, I can not find a helper function to detect it.

I tried to fix this by checking if expr is consistent with the span's file name of enclosing_block , but it still has an error from the include! macro after generating the **.fix file because we don't apply MachineApplicable for expr from include! macro.

The ui-test framework maybe need to add a feature that can ignore some errors(or external file's errors) which are excepted after auto-fix.

@relaxcn
Copy link
Contributor Author

relaxcn commented May 25, 2025

I searched the entire project and there doesn't seem to be any other test cases using the include! macro. Perhaps the problem lies with all the existing lints, which are not able to apply rustfix for expr that from include! macro.

@relaxcn
Copy link
Contributor Author

relaxcn commented May 25, 2025

You can have a look at this lint get_first.rs, it has the same problem in below code:

fn include_macro_test() {
    let z = &[2, 3, 5];
    let _ = include!("get.txt");
}

The get.txt:

z.get(0)

Maybe we need to discuss it with more people to get a final solution.

@relaxcn
Copy link
Contributor Author

relaxcn commented May 25, 2025

I found the definition of include! macro:

    #[stable(feature = "rust1", since = "1.0.0")]
    #[rustc_builtin_macro]
    #[macro_export]
    #[rustc_diagnostic_item = "include_macro"] // useful for external lints
    macro_rules! include {
        ($file:expr $(,)?) => {{ /* compiler built-in */ }};
    }

We can use the rustc_diagnostic_item, but it is not exist in rustc_span::sym module.

But I don't know how to get the data that before the include! macro is expanded. Maybe the AST? I'm not sure.

@llogiq
Copy link
Contributor

llogiq commented May 25, 2025

Yeah, let's not worry about it right now. I'm starting the FCP.

@relaxcn
Copy link
Contributor Author

relaxcn commented May 25, 2025

Yeah, let's not worry about it right now. I'm starting the FCP.

Thanks for your work.

I haven't had a break yet, there are always some young night owls.🫡

Copy link
Contributor

@samueltardieu samueltardieu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the code way much too complicated for what it is trying to achieve. Building a prefix tree by hand can be replaced by letting the compiler do the matching.

Moreover, if the first bytes/words of an IP address are given as constants, there is probably a good chance that the next bytes/words are as well, so we might evaluate them.

I would prefer something like that which is much shorter and gives the same functionality:

    if let ExprKind::Path(QPath::TypeRelative(
        Ty {
            kind: TyKind::Path(QPath::Resolved(_, func_path)),
            ..
        },
        p,
    )) = func.kind
        && p.ident.name == sym::new
        && let Some(func_def_id) = func_path.res.opt_def_id()
        && matches!(
            cx.tcx.get_diagnostic_name(func_def_id),
            Some(sym::Ipv4Addr | sym::Ipv6Addr)
        )
        && let Some(args) = args
            .iter()
            .map(|arg| {
                if let Some(Constant::Int(constant @ (0 | 1 | 127 | 255))) = ConstEvalCtxt::new(cx).eval(arg) {
                    u8::try_from(constant).ok()
                } else {
                    None
                }
            })
            .collect::<Option<SmallVec<[u8; 8]>>>()
    {
        let constant_name = match args.as_slice() {
            [0, 0, 0, 0] | [0, 0, 0, 0, 0, 0, 0, 0] => "UNSPECIFIED",
            [127, 0, 0, 1] | [0, 0, 0, 0, 0, 0, 0, 1] => "LOCALHOST",
            [255, 255, 255, 255] => "BROADCAST",
            _ => return,
        };
        span_lint_and_then(cx, IP_CONSTANT, expr.span, "hand-coded well-known IP address", |diag| {
            diag.span_suggestion_verbose(
                expr.span.with_lo(p.ident.span.lo()),
                "use",
                constant_name,
                Applicability::MachineApplicable,
            );
        });
    }

(you would need to add extern crate smallvec; to clippy_lints/src/lib.rs, but this doesn't cost anything as it is used in clippy_utils already)

This also gives a better suggestion with a visual replacement pattern. Maybe the error message could be tweaked though. Example:

error: hand-coded well-known IP address
  --> tests/ui/ip_constant.rs:38:13
   |
LL |     let _ = std::net::Ipv4Addr::new(127, 0, 0, 1);
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
help: use
   |
LL -     let _ = std::net::Ipv4Addr::new(127, 0, 0, 1);
LL +     let _ = std::net::Ipv4Addr::LOCALHOST;

@relaxcn
Copy link
Contributor Author

relaxcn commented May 26, 2025

Thanks for your remarks.

This is indeed a significant improvement. I’ve learned a lot from your suggestions.

The code has been modified.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Suggest LOCALHOST
5 participants