This is an archive of the discontinued LLVM Phabricator instance.

[ASTMatchers] Fix for https://bugs.llvm.org/show_bug.cgi?id=44364
ClosedPublic

Authored by njames93 on Dec 23 2019, 11:57 AM.

Details

Reviewers
aaron.ballman
Summary

Adds a new ASTMatcher condition called 'hasInitStatement()' that matches if, switch and range-for statements with an initializer. Reworked clang-tidy readability-else-after-return to handle variables in the if condition or init statements in c++17 ifs. Also checks if removing the else would affect object lifetimes in the else branch

Diff Detail

Event Timeline

njames93 created this revision.Dec 23 2019, 11:57 AM
Herald added a project: Restricted Project. · View Herald TranscriptDec 23 2019, 11:57 AM
Herald added a subscriber: cfe-commits. · View Herald Transcript
Eugene.Zelenko retitled this revision from Fix for https://bugs.llvm.org/show_bug.cgi?id=44364 to [ASTMatchers] Fix for https://bugs.llvm.org/show_bug.cgi?id=44364.Dec 23 2019, 6:34 PM
Eugene.Zelenko edited reviewers, added: aaron.ballman; removed: Restricted Project.
Eugene.Zelenko added a project: Restricted Project.

Does Clang support C++20's range-based for statements with initializer?

It does support the new range for loop, I'll add in a case for that in the AST matcher

aaron.ballman requested changes to this revision.Dec 24 2019, 7:20 AM

Thank you for this!

There's some missing test coverage for the changes to the clang-tidy check and for the new matcher. Also, the new matcher should be registered in clang\lib\ASTMatchers\Dynamic\Registry.cpp (in alphabetical order). Oh, and you should also run clang\docs\tools\dump_ast_matchers.py to regenerate the AST matcher documentation.

clang/include/clang/ASTMatchers/ASTMatchers.h
4300

Missing a full stop at the end of the sentence.

This revision now requires changes to proceed.Dec 24 2019, 7:20 AM

Thanks for the pointers, I'll amend those changes and update the patch, thanks for pointing it out for me. Probably be after boxing day though

njames93 updated this revision to Diff 235278.Dec 25 2019, 4:39 AM

I added test cases for the matchers and the check, wasn't able to add the CXXRangeFor to the matcher as that class uses a different dialect for the initializer statement handling. I also couldn't generate the new documentation as the script always fails on my machine, even without the changes i have made, resulting in the ASTMatcher html being empty

njames93 updated this revision to Diff 235300.Dec 25 2019, 3:36 PM
njames93 marked an inline comment as done.

Renamed 'hasInitStorage' to 'hasInitStatement'.
Added support for range for loops in 'hasInitStatement'.
'hasInitStatement' now has a match predicate on the init statement.
ReadabilityElseAfterReturn check now checks the init statement is a decl statement before disregarding it.
Added test cases for check and the matcher.
Regenerated the docs for the AST Matchers.

njames93 updated this revision to Diff 235301.Dec 25 2019, 3:43 PM
njames93 edited the summary of this revision. (Show Details)

fixed a spelling mistake in unit tests

aaron.ballman accepted this revision.Dec 26 2019, 9:55 AM

LGTM aside from a minor style nit.

clang/include/clang/ASTMatchers/ASTMatchers.h
4325–4326

Can drop the top-level const qualifier on the pointer (we don't typically do that). And can remove the extra parens around the return expression. e.g.,

const Stmt *Init = Node.getInit();
return Init && InnerMatcher.matches(*Init, Finder, Builder);
This revision is now accepted and ready to land.Dec 26 2019, 9:55 AM

So I wasn't happy with the vagueness of the else after return check implementation. Almost finished rewriting the check to properly handle the if statements with condition variables based on scope restrictions and where decls are used etc.

int *varInitAndCondition() {
  if (int *X = g(); X != nullptr) {
    return X;
  } else {
    h(&X);
    return X;
  }
}
int *varInitAndConditionUnusedInElseWithDecl() {
  int *Y = g();
  if (int *X = g(); X != nullptr) {
    return X;
  } else {
    int *Y = g();
    h(&Y);
  }
  return Y;
}

transforms to

int *varInitAndCondition() {
  int *X = g();
  if (X != nullptr) {
    return X;
  }
  h(&X);
  return X;
}
int *varInitAndConditionUnusedInElseWithDecl() {
  int *Y = g();
  if (int *X = g(); X != nullptr) {
    return X;
  } else {
    int *Y = g();
    h(&Y);
  }
  return Y;
}

There's a few more test cases but that's the general idea. I'm having a little trouble writing the test cases as I can't run them on my windows machine to verify they report correctly

njames93 updated this revision to Diff 235384.Dec 26 2019, 6:00 PM
njames93 edited the summary of this revision. (Show Details)

Think all the test cases are working, but they look like they could do with a clean up from someone who may know the clang-tidy-checks system better than myself

Should also point out that clang-format now has a better time reformatting as i expanded the replacements range to include the entire else block rather than the start and end braces

So I wasn't happy with the vagueness of the else after return check implementation. Almost finished rewriting the check to properly handle the if statements with condition variables based on scope restrictions and where decls are used etc.

int *varInitAndCondition() {
  if (int *X = g(); X != nullptr) {
    return X;
  } else {
    h(&X);
    return X;
  }
}
int *varInitAndConditionUnusedInElseWithDecl() {
  int *Y = g();
  if (int *X = g(); X != nullptr) {
    return X;
  } else {
    int *Y = g();
    h(&Y);
  }
  return Y;
}

transforms to

int *varInitAndCondition() {
  int *X = g();
  if (X != nullptr) {
    return X;
  }
  h(&X);
  return X;
}
int *varInitAndConditionUnusedInElseWithDecl() {
  int *Y = g();
  if (int *X = g(); X != nullptr) {
    return X;
  } else {
    int *Y = g();
    h(&Y);
  }
  return Y;
}

There's a few more test cases but that's the general idea. I'm having a little trouble writing the test cases as I can't run them on my windows machine to verify they report correctly

Hmm, I'm not convinced the new behavior is better in all cases, and I suspect this may boil down to user preference (suggesting we may want an option to control the behavior). There are competing stylistic decisions to be made for varInitAndCondition() -- one is the else after a return, but the other is the scope of the variable X. The behavior now forces the user to prefer removing the else after the return over hiding the variable X in a logical scope. For the contrived example provided, this isn't a huge deal because the function is small. However, in real-world code with larger function bodies, I can see wanting to prefer the name hiding. WDYT?

Sorry I didn't make it obvious with the test cases. The fix will never extend the lifetime of variables either in the condition or declared in the else block. It will only apply if the if is the last statement in its scope. Though I should check back through the scope for any declarations that have the same identifier as those in the if statement which would cause an error if they were brought out of the if scope

Adding checks to see if there are any declarations already in the ifs contained scope is really starting to bloat the code while trying to cover every edge case.

njames93 updated this revision to Diff 235573.Dec 30 2019, 3:05 AM

Fixed a few more edge cases with test cases

njames93 updated this revision to Diff 235661.Dec 30 2019, 4:35 PM

Fixed a few of the test cases failing

Sorry I didn't make it obvious with the test cases. The fix will never extend the lifetime of variables either in the condition or declared in the else block. It will only apply if the if is the last statement in its scope. Though I should check back through the scope for any declarations that have the same identifier as those in the if statement which would cause an error if they were brought out of the if scope

Oh, thank you for the clarification, I hadn't picked up on that.

clang-tools-extra/clang-tidy/readability/ElseAfterReturnCheck.cpp
21

Hmm, I just realized we missed co_return for this check. That's a separate patch, though, so don't feel the need to change anything about this here.

160

The use of dyn_cast_or_null is suspicious in these methods -- if Node is null, then we'll get into the else clause and promptly dereference a null pointer. My guess is that these should be dyn_cast calls instead, but if Node can truly be null, that case should be handled.

165

const auto *, no? (Same below.)

166

const auto * for sure. (Same below.)

168

warn -> Warn
and should have a full stop at the end of the comment. The same applies to the other instances of this comment.

192

const auto * (this comment applies throughout the patch -- we don't want to deduce qualifiers or pointer/references, so they should be spelled out explicitly.)

201

const auto *, no?

238

Please don't use auto here as the type is not spelled out in the initialization. Also, drop the top-level const qualifier. Same elsewhere.

njames93 updated this revision to Diff 235752.Jan 1 2020, 3:10 AM

hopefully adhered to all conventions :)

njames93 marked 8 inline comments as done.Jan 1 2020, 3:57 AM

I'm in two minds about issuing a warning when scope restrictions prevent a fix. Do you think creating an option to enable or disable emitting warnings for cases where the scope prevents a fix would be a good idea?

I'm in two minds about issuing a warning when scope restrictions prevent a fix. Do you think creating an option to enable or disable emitting warnings for cases where the scope prevents a fix would be a good idea?

It's not uncommon for fixits to only be generated under specific circumstances, so I'm wondering what your concern is with warning when we can't provide a fixit? The cases I am thinking about all seem reasonable to diagnose (are true positives) without fixing, but maybe you have different circumstances in mind.

I'm in two minds about issuing a warning when scope restrictions prevent a fix. Do you think creating an option to enable or disable emitting warnings for cases where the scope prevents a fix would be a good idea?

It's not uncommon for fixits to only be generated under specific circumstances, so I'm wondering what your concern is with warning when we can't provide a fixit? The cases I am thinking about all seem reasonable to diagnose (are true positives) without fixing, but maybe you have different circumstances in mind.

Right now an issue is raised for every else after return flag, but not all else after return flags can be fixed due to declaration statements and scope issues. My suggestion is that you can choose to warn about those cases or not. For example a developer may want else after return for when they need to limit the scope and getting a warning for it may be undesirable.

I'm in two minds about issuing a warning when scope restrictions prevent a fix. Do you think creating an option to enable or disable emitting warnings for cases where the scope prevents a fix would be a good idea?

It's not uncommon for fixits to only be generated under specific circumstances, so I'm wondering what your concern is with warning when we can't provide a fixit? The cases I am thinking about all seem reasonable to diagnose (are true positives) without fixing, but maybe you have different circumstances in mind.

Right now an issue is raised for every else after return flag, but not all else after return flags can be fixed due to declaration statements and scope issues. My suggestion is that you can choose to warn about those cases or not. For example a developer may want else after return for when they need to limit the scope and getting a warning for it may be undesirable.

Okay, I can see the value in having an option for that -- especially given that silencing the diagnostic would add // NOLINT noise to the code. Do you think the option should default to diagnosing all cases, even ones without a fixit?

I'm in two minds about issuing a warning when scope restrictions prevent a fix. Do you think creating an option to enable or disable emitting warnings for cases where the scope prevents a fix would be a good idea?

It's not uncommon for fixits to only be generated under specific circumstances, so I'm wondering what your concern is with warning when we can't provide a fixit? The cases I am thinking about all seem reasonable to diagnose (are true positives) without fixing, but maybe you have different circumstances in mind.

Right now an issue is raised for every else after return flag, but not all else after return flags can be fixed due to declaration statements and scope issues. My suggestion is that you can choose to warn about those cases or not. For example a developer may want else after return for when they need to limit the scope and getting a warning for it may be undesirable.

Okay, I can see the value in having an option for that -- especially given that silencing the diagnostic would add // NOLINT noise to the code. Do you think the option should default to diagnosing all cases, even ones without a fixit?

Definitely default to diagnosing everything, that's how my current implementation looks right now. Also is there a way to do options as bool because I don't particularly like putting 1 for true in the config file

I'm in two minds about issuing a warning when scope restrictions prevent a fix. Do you think creating an option to enable or disable emitting warnings for cases where the scope prevents a fix would be a good idea?

It's not uncommon for fixits to only be generated under specific circumstances, so I'm wondering what your concern is with warning when we can't provide a fixit? The cases I am thinking about all seem reasonable to diagnose (are true positives) without fixing, but maybe you have different circumstances in mind.

Right now an issue is raised for every else after return flag, but not all else after return flags can be fixed due to declaration statements and scope issues. My suggestion is that you can choose to warn about those cases or not. For example a developer may want else after return for when they need to limit the scope and getting a warning for it may be undesirable.

Okay, I can see the value in having an option for that -- especially given that silencing the diagnostic would add // NOLINT noise to the code. Do you think the option should default to diagnosing all cases, even ones without a fixit?

Definitely default to diagnosing everything, that's how my current implementation looks right now.

SGTM

Also is there a way to do options as bool because I don't particularly like putting 1 for true in the config file

Not to my knowledge.

Definitely default to diagnosing everything, that's how my current implementation looks right now.

SGTM

Also is there a way to do options as bool because I don't particularly like putting 1 for true in the config file

Not to my knowledge.

I just did a quick grep and found that all checks that take bools for config options use a 1 for true, 0 for false, so I guess best stick to convention rather than hack at OptionsView to get and set bools

njames93 updated this revision to Diff 235775.Jan 1 2020, 10:29 AM

Added option to disable warning when an automatic fix can't be applied due to scope restrictions of variables, default option is to show all warnings

aaron.ballman accepted this revision.Jan 1 2020, 10:45 AM

The new option LGTM but one of the tests can be updated to have a less complex RUN line.

clang-tools-extra/test/clang-tidy/checkers/readability-else-after-return.cpp
2–3

You don't need to explicitly set it to 1 here.

njames93 updated this revision to Diff 235788.Jan 1 2020, 11:39 AM

removed the unnecessary explicit setting of WarnOnUnfixable value for test case

njames93 marked an inline comment as done.Jan 1 2020, 11:41 AM

The new option LGTM but one of the tests can be updated to have a less complex RUN line.

that was just put in when i was initially testing the behavoiur, removed it now

njames93 updated this revision to Diff 235791.Jan 1 2020, 11:53 AM

small reformat

aaron.ballman accepted this revision.Jan 1 2020, 12:47 PM

Still LG -- do you need someone to land this on your behalf?

Yes please, I'm still very new to the llvm contributing system so I would have no idea what to do next.

aaron.ballman closed this revision.Jan 2 2020, 10:39 AM

Thank you for the patch, I've committed on your behalf in ec3d8e61b527c6312f77a4dab095ffc34e954927