Skip to content
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

warn about !$x == $y, !$x =~ /abc/, and similar constructs #22507

Merged
merged 2 commits into from
Aug 31, 2024

Conversation

mauke
Copy link
Contributor

@mauke mauke commented Aug 13, 2024

Commit 2f48e46 introduced a warning for logical negation as the left operand of the isa operator, which likely indicates a precedence problem (i.e. the programmer wrote ! $x isa $y when they probably meant !($x isa $y)).

This commit extends the warning to all (in)equality comparisons (==, !=, <, >, <=, >=, eq, ne, lt, gt, le, ge) as well as binding operations (=~, !~).

As an indication that such a warning is useful in the real world, the core currently contains two files with (likely broken) code that triggers this warning:

  • ./cpan/Test-Simple/lib/Test2/Hub.pm
  • ./cpan/Scalar-List-Utils/t/uniqnum.t

@mauke
Copy link
Contributor Author

mauke commented Aug 13, 2024

Shout-outs to Dual-Life/Scalar-List-Utils#134 and Test-More/test-more#999.

@jkeenan
Copy link
Contributor

jkeenan commented Aug 14, 2024

Commit 2f48e46 introduced a warning for logical negation as the left operand of the isa operator, which likely indicates a precedence problem (i.e. the programmer wrote ! $x isa $y when they probably meant !($x isa $y)).

This commit extends the warning to all (in)equality comparisons (==, !=, <, >, <=, >=, eq, ne, lt, gt, le, ge) as well as binding operations (=~, !~).

As an indication that such a warning is useful in the real world, the core currently contains two files with (likely broken) code that triggers this warning:

* ./cpan/Test-Simple/lib/Test2/Hub.pm

* ./cpan/Scalar-List-Utils/t/uniqnum.t

This change is also causing test failures in t/porting/utils.t. I am very skeptical as to the need for this warning and I predict it will be very unpopular.

@mauke
Copy link
Contributor Author

mauke commented Aug 14, 2024

This change is also causing test failures in t/porting/utils.t.

That's the Test2::Hub issue I mentioned above. Once we sync the latest Test::Simple from CPAN, the error should disappear.

I am very skeptical as to the need for this warning and I predict it will be very unpopular.

Why? It found two long-standing bugs, one in Test2 and one in the test suite of List::Util, basically for free.

@haarg
Copy link
Contributor

haarg commented Aug 14, 2024

Taking a quick look at CPAN, most uses of this construct are intentionally comparing booleans, so this warning would be a false positive.

@mauke
Copy link
Contributor Author

mauke commented Aug 14, 2024

@haarg Can you show me a few examples you found? I want to see if it is possible to avoid most false positives or if I should just throw the whole thing away.

Note that it doesn't warn if ! appears on both sides of the comparison, so code like the following is "safe":

if ( !$block != !$orig ) {                         # ./dist/IO/lib/IO/Socket.pm
ok !!$exp == !!($str1 =~ $re), "$desc str =~ qr";  # ./ext/XS-APItest/t/callregexec.t
ok(!!-e " . . " == !!opendir(FOO, " . . ");        # ./t/win32/stat.t

Commit 2f48e46 introduced a warning for logical negation as the
left operand of the `isa` operator, which likely indicates a precedence
problem (i.e. the programmer wrote `! $x isa $y` when they probably
meant `!($x isa $y)`).

This commit extends the warning to all (in)equality comparisons (`==`,
`!=`, `<`, `>`, `<=`, `>=`, `eq`, `ne`, `lt`, `gt`, `le`, `ge`) as well
as binding operations (`=~`, `!~`).

As an indication that such a warning is useful in the real world, the
core currently contains two files with (likely broken) code that
triggers this warning:

  - ./cpan/Test-Simple/lib/Test2/Hub.pm
  - ./cpan/Scalar-List-Utils/t/uniqnum.t
@mauke
Copy link
Contributor Author

mauke commented Aug 17, 2024

Taking a quick look at CPAN, most uses of this construct are intentionally comparing booleans, so this warning would be a false positive.

I can't replicate this.

I did a cursory search on https://grep.metacpan.org, only looking for cases where the negated LHS is a plain scalar. I filtered out all the irrelevant results (matches in POD, strings, regexes, shell scripts, etc), analyzed the remaining cases and grouped them in three buckets.

First, the group of false positives:

Distribution File Code
Devel-Trepan t/10test-db-eval.t ok (!$msg == $is_good, "${code}" . ($msg ? ": $msg" : ''));
MsgPack-RPC t/encoding-decoding.t ok !!$next == $_;

These are unfortunate, but they should only produce a little extra output during testing. (And they are easily patched if needed, e.g. with ($msg xor $is_good) or !!$next == !!$_.) Normal users of these modules are not affected.

Next we have the group of potential false positives, but the files in question don't use warnings and so are unaffected:

Distribution File Code
Math-Algebra-Symbols t/complex.t ok( !$i == $i);
Math-Algebra-Symbols t/unit.t ok( !$i == $i );
Math-Zap t/vector.t ok(!$x == 1);
Math-Zap t/vector2.t ok(!$x == 1);
Object-Boolean t/03-other.t ok !$b eq No, 'not b is no';
Object-Boolean t/03-other.t ok !$b eq 'No', 'not is "No"';
Object-Boolean t/03-other.t ok !$a eq 'Yes', 'yes';
Object-Boolean t/03-other.t ok !$d eq 'No', 'no';

(Math-Zap and Math-Algebra-Symbols both use a non-standard overloading of !; e.g. the one for vectors actually returns the vector length.)

And in the third group there is the list of true positives, i.e. actual bugs that are potentially found by this warning (I think this list is incomplete because for one of my searches grep.metacpan.org stopped updating after "Piddy"):

Distribution File Code
AnyData lib/AnyData.pm && !$createMode eq 'o';
AnyEvent-Yubico lib/AnyEvent/Yubico.pm if(! $signature eq $self->sign($response)) {
App-BCSSH lib/App/BCSSSH/Command/ssh.pm if ($auth_key && ! $auth_key ne $key) {
App-SourcePlot lib/App/SourcePlot.pm if ((!$yr =~ /$sety/) || $setm != $mo || $setd != $md) {
Array-KeepGrepped t/01_basic.t my @grep = grep { !$_ =~ m#a# } @test;
Array-KeepGrepped t/01_basic.t my (undef, @kgrep) = kgrep { !$_ =~ m#a# } @test;
Audio-Nama lib/Audio/Nama/Engine.pm if( ! $return_value == 256 ){
BalanceOfPower lib/BalanceOfPower/Role/Warlord.pm @attacker_coalition = grep { ! $attack_now eq $_ } @attacker_coalition;
Bio-NEXUS exec/nextool.pl if (! $s =~ /^\s*(\d+(-\d+)?)([,\s]\s*\d+(-\d+)?)*\s*$/ ) {
Bio-NEXUS exec/readin_tcoffee.pl if (! $tcoff =~ /^T-COFFEE/i) { die "\n\tError: File does not start with 'T-COFFEE'; does not appear to be a T-COFFEE file\n\n"; }
Bio-NEXUS lib/Bio/NEXUS/CharactersBlock.pm if ( $token =~ /^\d+$/ && !$charnum > 0 ) { $charnum = $token; next; }
Bio-PhyloNetwork lib/Bio/PhyloNetwork.pm if (! $subrtlng eq '') {
Bio-PhyloNetwork lib/Bio/PhyloNetwork.pm if (! $subrttype eq '') {
BioPerl Bio/PhyloNetwork.pm if (! $subrtlng eq '') {
BioPerl Bio/PhyloNetwork.pm if (! $subrttype eq '') {
Blitz lib/Blitz/Validate.pm elsif (! $type =~ /^(list|alpha|number|udid)$/) {
Business-WorldPay-Junior Junior.pm if ( ! $difference < -($hr_trans->{amount} * 0.05) )
CSAF lib/CSAF/Validator/OptionalTests.pm if (!$url =~ /^https\:/) {
Chooser lib/Chooser.pm if (!$value =~ /^\%/){
Crypt-Simple-SMIME lib/Crypt/Simple/SMIME.pm if ( ! $var =~ /\n/ && -f $var ) {
DBI-Easy lib/DBI/Easy/Record/Collection.pm if ($limit > $MAX_LIMIT or ! $limit > 0) {
DBIx-Class-InflateColumn-DateTime-WithTimeZone lib/DBIx/Class/InflateColumn/DateTime/WithTimeZone.pm if ( !$ic_dt_method || !$ic_dt_method =~ /(?: datetime | timestamp )/x ) {
DBIx-Frame DBIx/Frame/CGI.pm if ( ( $labels[$i] eq 'First' && ! $first > 0 )
DBIx-JCL lib/DBIx/JCL.pm if ( ! $severity eq 'MESSAGE' ) {
DNS-Bananafonana Bananafonana.pm if (! $markup =~ /[-_\.]/) {
Database-ManagedHandle t/managed-singleton-tempdb-advanced.t $dbh->begin_work() if ( !$driver eq 'CSV' );
Database-ManagedHandle t/managed-singleton-tempdb-advanced.t $dbh->commit if ( !$driver eq 'CSV' );
Database-ManagedHandle t/managed-singleton-tempdb-advanced.t $dbh->begin_work() if ( !$driver eq 'CSV' );
Database-ManagedHandle t/managed-singleton-tempdb-advanced.t $dbh->commit if ( !$driver eq 'CSV' );
Db-Documentum test.pl if (! $OS =~ /Win/i) {
Debug-Statements lib/Debug/Statements.pm $options = "-$options" if ! $options =~ /^-/;
E2-Interface E2Node.pm if( !$r =~ /<author .*?user_id="(.*?)"/s ) {
Embperl eg/web/db/epwebapp.pl $buri .= '/' if (!$buri =~ m#/$#) ;
FFI-Platypus lib/FFI/Probe.pm my $complex = !!$type =~ /complex/;
File-SmartTail lib/File/SmartTail.pm if (!$rmtopts =~ /\B-type\s+\w/) {
Forks-Super t/42l-filehandles.t && !$line1 !~ /[\x{0100}-\x{CCCC}]/,
FreeBSD-Pkgs-FindUpdates lib/FreeBSD/Pkgs/FindUpdates.pm if (!$pkgname =~ /^bsdpan-/) {
GRID-Machine scripts/gmdb $sshcommand = $1 if !$sshcommand and !$hostdesc =~ /^#gm\s+sshcommand\s+'([^'\n]*)'/m;
Games-Axmud lib/Games/Axmud/Client.pm if (! $line =~ /^#!\s*(.*perl\S*)/) {
Graphics-HotMap lib/Graphics/HotMap.pm if ( ! $value > 0) {
HTML-Extract lib/HTML/Extract.pm if(!$command eq ""){
HTTP-WebTest-XMLParser lib/HTTP/WebTest/XMLParser.pm if (! $parent eq 'list') {
IBM-LoadLeveler Makefile.PL if ( ! $LLVER eq "UNKNOWN" )
IO-Util lib/IO/Util.pm &&! $mtime > $PARSING_CACHE{$type}{$path}{mtime} # and not old
Image-Math-Constrain lib/Image/Math/Constrain.pm !! $value =~ /^[1-9]\d*$/;
Image-Math-Constrain lib/Image/Math/Constrain.pm !! $value =~ /^[1-9]\d*$/;
Lab-Measurement-Legacy lib/Lab/Instrument/YokogawaGS200.pm if ( !$function eq 'VOLT' ) {
Lemonldap-Handlers-CAS lib/Lemonldap/Handlers/ValidateCAS.pm if ( ( !$controle == 0 ) and ( defined $controle->{string} ) ) {
List-Uniqnum t/01uniqnum.t $nanish != 0 && !$nanish != $NaN
List-Util-MaybeXS t/uniqnum.t $nanish != 0 && !$nanish != $NaN
Log-GELF-Util lib/Log/GELF/Util.pm if ( ! $key =~ /^[\w\.\-]+$/ ) {
Mail-BIMI lib/Mail/BIMI/VMC.pm next if ! $alt_name =~ /^dns:/;
Mail-Milter-Authentication lib/Mail/Milter/Authentication/Protocol/SMTP.pm if ( ! $line =~ /250/ ) {
MarpaX-Languages-C-AST bin/c2ast $lexemeCallbackHash{nbLines} = ($preprocessedOutput =~tr/\n/\n/ + ! $preprocessedOutput =~ /\n\z/);
Math-Base-Convert Convert.pm ! $slen > $maxdlen{$fbase}) {
Mediawiki-Spider lib/Mediawiki/Spider.pm if(!$1 eq ""){
Mediawiki-Spider lib/Mediawiki/Spider.pm if($topush=~/Category/ && !$word=~/Category/){
Mojolicious-Plugin-Iconify lib/Mojolicious/Plugin/Iconify/API.pm next if ( !$file =~ /.json/ );
Myco lib/Myco/QueryTemplate.pm if ! $filter_string =~ /$result/;
NCBIx-Geo lib/NCBIx/Geo.pm if (! $data_dir =~ m#/$# ) { $self->set_data_dir( $data_dir . '/' ); }
Nes lib/Nes.pm return if !$block =~ /$self->{'tag_start'}\s*$self->{'tag_nes'}/;
Net-Amazon-S3 lib/Net/Amazon/S3/Request/Role/HTTP/Header.pm (init_arg => undef) x!! $name =~ m/^_/,
Net-Blogger lib/Net/Blogger/Engine/Userland.pm if (! $child =~ /^(Net::Blogger::Engine::(Manila|Radio))$/) {
Net-DNS-Resolver-DoH lib/Net/DNS/Resolver/DoH.pm next if ! $ns =~ /https:\/\//;
Net-DNS-Resolver-DoH lib/Net/DNS/Resolver/DoH.pm next if ! $ns =~ /\{dns\}/;
Net-DirectConnect lib/Net/DirectConnect/pslib/psmisc.pm return undef if $ip =~ /^(?:0|127)\./ and !$host =~ /^(?:0|127)\./;
Net-PSYC PSYC/Event/Event.pm next if($flags && !$flags =~ /$_/);
Net-Telnet-Trango scripts/su.cgi if ( !$suid =~ /^\d+$/ ) {
Neuron Neuron.pm if(! $s =~ /^Hiddenlayer:/) {
OOPS t/upgrade1004.t rcon if $docommit & 2**$tn && ! $tn < $#func;
PDF-API2 contrib/text2pdf.pl if ( ! $left > 756 ) {
Perl-Repository-APC scripts/buildaperl die "patchlevel not > 0" if length $patchlevel && ! $patchlevel > 0;
Piddy lib/Piddy.pm if (! $rpid eq getppid()) {
SAS-Parser lib/SAS/Parser.pm if (! $mfunc =~ m/$mfuncs/) {
Sakai-Nakamura lib/Sakai/Nakamura/Content.pm if ( !$content_filename =~ /.*\..*/x ) {
Spreadsheet-HTML lib/Spreadsheet/HTML/Presets/Beadwork.pm my @lines = grep ! $_ =~ /^\s*$/, split /\n/, $args->{art};
Startup Startup.pm return $txt if !$txt=~/\$/;
TX lib/TX.pm delete @{$I->cache}{grep( ($xor xor !$_=~$re), keys %{$I->cache} )};
TX lib/TX.pm delete @{$I->cache}{grep( !$_=~$re, keys %{$I->cache} )};
TX lib/TX.pm delete @{$I->cache}{grep( ($xor xor !$_=~$re), keys %{$I->cache} )};
Text-Macro Macro.pm if ( ! $path =~ m!/$! ) {

@tonycoz
Copy link
Contributor

tonycoz commented Aug 19, 2024

I like the change, but it would be nice to see more on the false positives.

@mauke
Copy link
Contributor Author

mauke commented Aug 21, 2024

I just pushed a change to get rid of a class of false positives (where ! on the right-hand side has been constant-folded away).

@haarg, do you have more examples of false positives?

@mauke
Copy link
Contributor Author

mauke commented Aug 30, 2024

There have been no new comments in a while. I still think this warning is a good idea, but to collect more feedback, I'd like to merge it into blead and see if anything breaks on CPAN. If it turns out to be too noisy, we can still revert it.

Objections?

@leonerd
Copy link
Contributor

leonerd commented Aug 30, 2024

I think it seems reasonable. The only big class of false-positive case I can think of is the !$x == !$y style of comparing booleans, but I see that is already specifically handled and doesn't issue a warning. I think it's good enough to try it out at least, and see what the result is.

@mauke mauke merged commit ebf6b88 into Perl:blead Aug 31, 2024
33 checks passed
@mauke mauke deleted the warn-not-vs-cmp branch August 31, 2024 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants