This is an archive of the discontinued LLVM Phabricator instance.

[utils][filecheck-lint] Add filecheck-lint, a linter for FileCheck directives.
ClosedPublic

Authored by bchetioui on Jan 11 2023, 8:08 AM.

Details

Summary

FileCheck is a versatile tool for testing compiler output regardless of syntax.
FileCheck achieves this versatility by processing only input lines in which it
detects a valid CHECK directive, and ignoring everything else.

This comes at a price: if a directive is not typed properly, it is not
processed by FileCheck, and the test code it precedes is effectively disabled.
This results in the illusion that the code is tested, while the test may have
actually never run.

This scenario is not hypothetical, see the fixes introduced in, e.g.
https://github.com/tensorflow/tensorflow/commit/48cacf049f3d6ed3f289ccc48ec50491b6d8d9a8,
https://reviews.llvm.org/D139698, https://reviews.llvm.org/D139636,
https://github.com/iree-org/iree/pull/11693.

The findings corrected in the above changes originate in filecheck-lint.
In a given test file, filecheck-lint uses the edit distance between anything
that looks like a FileCheck directive and the set of locally valid directives
to detect likely misspelled directives.

The tool is not yet feature complete---e.g. it does not parse custom comment
prefixes to exclude them from reporting---but it already yields useful results,
as demonstrated above.

Diff Detail

Event Timeline

bchetioui created this revision.Jan 11 2023, 8:08 AM
Herald added a project: Restricted Project. · View Herald TranscriptJan 11 2023, 8:08 AM
bchetioui requested review of this revision.Jan 11 2023, 8:08 AM
Herald added a project: Restricted Project. · View Herald TranscriptJan 11 2023, 8:08 AM

This seems to be spread over more files/directories than is typical for small tools like this.
Would you consider:

  • folding main into filecheck_lint.py, under if __name__=='__main__' so it still works as a library
  • dropping the filecheck_lint directory
  • moving the contents of README.md into comments/docstring of the script itself

I think the test is OK sitting loose under utils next to the main file (but only if something actually runs it)

llvm/utils/filecheck_lint/README.md
3 ↗(On Diff #488229)

replace with a normal space?

16 ↗(On Diff #488229)

I don't see any reason users should care about customizing this - we should set a sensible default, and the set of directives doesn't vary wildly between tests.

Fine if you want to have it for testing or some external use, but I wouldn't document it - it's distracting.

Sorry, hit enter too soon on previous round.

Generally this looks really useful, and finds real bugs!

Most of the comments below are of the form:

  • the implementation seems overly complex and general, I think being short/simple/direct is more useful for maintenance than being "good python code", as this is not production code and will be read/edited by people who don't know much python
  • for my taste there's too much boilerplate comments repeating the names of things, and not enough explaining how this piece of code is contributing to solving the probelm
llvm/utils/filecheck_lint/filecheck_lint.py
19

this isn't used anywhere - if there isn't a concrete need I'd suggest just making it a constant, then you can drop argparse and just iterate over argv?

(If you want to use a different config out of tree that's not unreasonable, but it's not clear to me why that would be the case)

llvm/utils/filecheck_lint/filecheck_lint/filecheck_lint.py
8 ↗(On Diff #488229)

there are a lot of fine-grained comments here about the implementation (e.g. what a class represents) but it's hard to get an overall picture of what this library does and how.

I think an example would go a long way here, somethng like:

Consider a broken test foo.cpp:

// RUN: clang -cc1 -ast-dump %s | FileCheck %s --check-prefix=NEW
// RUN: clang -cc1 -ast-dump %s -std=c++98 | FileCheck %s --check-prefix=OLD
auto x = 42;
// NEWW: auto is a c++11 extension
// ODL-NOT: auto is a c++11 extension

First we detect FileCheck directive prefixes by looking for --check-prefix flags.
Here we get {CHECK, NEW, OLD}, so our directive names are
{CHECK, NEW, OLD, CHECK-NOT, NEW-NOT, ...}.

Then we look for lines that look like directives: these are of the form FOO: at the beginning of a line or a comment.
If any of these are a "near-miss" for a directive name, then we suspect this is a typo.
39 ↗(On Diff #488229)

If there's nothing non-obvious to say about an arg (the type hints already say they're strings), don't say anything? Similarly the "Returns:" just echoes the first line.

(I'm not a python person, maybe there's some structural reason this comment needs to be this way, but other files seem mostly not to have them)

62 ↗(On Diff #488229)

nit: FileRange? (both avoiding the abbreviation & the impression that it describes a point only)

83 ↗(On Diff #488229)

"emit" doesn't belong here I think.

Maybe more precisely: "Stores information about one typo and possibly its fix" (is the fix optional?)

I'm not sure the second paragraph or the attributes adds anything nonobvious (Findings isn't a concept that makes sense here)

113 ↗(On Diff #488229)

nit: US english is much more common in LLVM, so probably "misspelled"

124 ↗(On Diff #488229)

We should also support the directive being at the start of a line-comment, even if the line is otherwise non-empty.

rg "\s*[^ ]\s*// CHECK" clang/test shows many examples e.g:

../clang/test/Refactor/LocalRename/NoSymbolSelectedError.cpp
4:class Baz { // CHECK: [[@LINE]]:1: error: there is no symbol at the given location
131 ↗(On Diff #488229)

this seems overly complicated: why not just always accept all comment markers in any type of file?

Eyeballing "(?:^|#|//|;)\s*([A-Z0-9-_]+)\s*:", I don't see false positives.

140 ↗(On Diff #488229)

it's not really clear what's to be done here: improved how? how can someone tell when it's time to delete this TODO?

143 ↗(On Diff #488229)

I'd suggest restricting to uppercase as this is a ~universal convention and will avoid big classes of false positives, allowing to simplify elsewhere

148 ↗(On Diff #488229)

up to you, but I'm not sure getting convenient access to the line number is worth doing everything linewise and looping.
You could make FilePos's constructor compute the line/column, then that class would do something useful!

(Not that it matters, but I suspect it also performs better in practice to run the regex over a big buffer, and actually needing the line number is the very rare error case)

187 ↗(On Diff #488229)

This seems like it could be much simpler: just take the whole file, match against a regex, and always split the captured arg on , since a prefix will never have one.

def parse_custom_prefixes(code): 
  for m in re.finditer(r'--check-prefix(?:es)?[ =](\S+)', code):
    for prefix in m.group(1).split(','):
      yield prefix
188 ↗(On Diff #488229)

this comment doesn't really say anything beyond what the function signature already says.

Maybe an example? (in the code: .... the prefixes are ...)

259 ↗(On Diff #488229)

why have two functions here instead of one? can't we just have the caller pass in the file content?

268 ↗(On Diff #488229)

this just echoes the function name

276 ↗(On Diff #488229)

nit: make _suffixes = {"", "-NOT", "-DAG"} etc instead?

292 ↗(On Diff #488229)

up to you, many of these functions could be slightly terser with yield rather than create/append/return an array

312 ↗(On Diff #488229)

this is a redundant second check for _ignore, you already added it to all_directives above

llvm/utils/filecheck_lint/tests/filecheck_lint_test.py
1 ↗(On Diff #488229)

this test isn't being run by anything.
you can leave it out (most utils aren't tested, this is already a tool for improving tests, recursion needs to stop at some point!)

But thinking about it more, I think it's harmless enough and probably helps when developing it.
There are a few other tools that use unittest.TestCase, AFAIK nothing runs those either.

bchetioui updated this revision to Diff 488565.Jan 12 2023, 2:46 AM
bchetioui marked 19 inline comments as done.

Address review comments.

Thanks for the thorough review, Sam, I think this is much better!

I have addressed your comments in my latest revision. Let me know if something is still amiss.

llvm/utils/filecheck_lint/README.md
16 ↗(On Diff #488229)

I removed the option and removed this line from the documentation.

llvm/utils/filecheck_lint/filecheck_lint/filecheck_lint.py
8 ↗(On Diff #488229)

Thanks, that's indeed much better.
I added a slight variation of your comment there, hope that's OK.

39 ↗(On Diff #488229)

The structure was meant to satisfy an overly eager linter.
That is not a good reason, and I agree it is not particularly useful. Removed.

83 ↗(On Diff #488229)

Done. (The fix is not optional.)

124 ↗(On Diff #488229)

Oops, in fact, this is a case of the documentation being out of date with regards to the code.
In fact, the very case that you are outlining is tested in test_find_potential_directives_comment_prefix and works properly.
Corrected the documentation to reflect that, thanks!

131 ↗(On Diff #488229)

A comment marker is not strictly necessary for something to be parsed as a directive by FileCheck---meaning I'd like to keep the option for directive prefix to be optional. But if it is always optional, then I am afraid we may see a host of false positives, since anything preceding a colon would always be matched for every type of file.

I personally prefer it like this, but if you'd rather, I could change "directive_prefix" to a boolean "match_comment_marker". When enabled, I would match any comment marker. WDYT?

Apart from the comment markers, I think the regex you suggest will not yield false positives, but that it may allow more "false negatives" to pass through. E.g., in https://github.com/tensorflow/tensorflow/commit/48cacf049f3d6ed3f289ccc48ec50491b6d8d9a8, one of typos contained lowercase letters, and another contained a space instead of a dash. These would not be caught by the regex you suggest, so I would like to keep the regex as is. WDYT?

140 ↗(On Diff #488229)

I removed this unclear TODO. One improvement I was thinking about (but that does not actually concern the regex directly) is that something like "// some inline comment CHCK:" gives us a match with "some inline comment CHCK:" right now. We should probably also match "inline comment CHCK:", "comment CHCK:", and "CHCK:", since FileCheck may match either of those.

While more correct, I haven't yet validated that this is fine wrt false positives, so I have chosen to omit this from the initial version of the code.

143 ↗(On Diff #488229)

ACK, but I don't think restricting to uppercase will yield better results, as explained above in the "directive_prefix" comment.

By the way, I have run the linter over e.g. all the .ll test files in the LLVM repo, and I do not recall matching false positives related to case. (Either way, the number of false positives has actually been surprisingly low.)

148 ↗(On Diff #488229)

I think you are right re. performance, and this is cleaner. Made the changes are you suggest, thanks!

187 ↗(On Diff #488229)

Indeed, that's much better. I modified the regex slightly to match also other types of valid flag-settings that occur.
(That was actually one of the TODOs above, should have just done it :-)).

292 ↗(On Diff #488229)

Thanks, I made find_potential_directives and find_directive_typos generators, as you suggest.

312 ↗(On Diff #488229)

The purpose of this check is to not report on detected typos when the likely intended directive is one we would ignore anyway.
This is particularly useful if someone uses very short custom CHECK prefixes (e.g. given a check prefix "DO", "ToDo" would be close enough to "DO" that it could be a typo, but is actually closer to "TODO". Reporting it would likely be a false positive).

I suggest we leave it as is, WDYT?

llvm/utils/filecheck_lint/tests/filecheck_lint_test.py
1 ↗(On Diff #488229)

If you think this is harmless enough, I would like to keep some tests next to the tool. (If not, I will keep them on my end for personal use, but that seems worse.)
They don't need to be run automatically, but I do find them helpful for development.

Thanks! This all looks pretty good to me apart from the complexity around comment markers, detecting which comment markers to use for which language etc.
I'd really like if you could either:

  • decide we don't care about comment markers, and drop them entirely
  • decide we do care, and hardcode (any comment marker|start-of-line), dropping the optional-ness and language detection
  • explain why it's important that this feature is sometimes on/sometimes off, we need to know exactly the per-language comment marker etc
llvm/utils/filecheck_lint/filecheck_lint/filecheck_lint.py
131 ↗(On Diff #488229)

A comment marker is not strictly necessary for something to be parsed as a directive by FileCheck

Sure, but you're using heuristics, and have to make some decisions. If all filters are loose (don't require caps, allow spaces, don't require it to be at the start of a line/comment) then you're going to have lots of false positives.

In the code the comment marker is not optional: you always pass one at the callsite. So the code handling directive_prefix is None is dead & I think we should delete it. Alternatively, if you want to allow directives to appear anywhere, delete the case that handles directive_prefix. Unless it's really important, we don't need to define an API that lets you turn this on or off.

Sure, that's why we would have ^ in the alternation: it will match at the beginning of the line (I guess you have to set re.MULTILINE in python for this)

Apart from the comment markers, I think the regex you suggest will not yield false positives, but that it may allow more "false negatives" to pass through

Fair enough. Allowing spaces *and* lowercase letters seems very permissive. Matching too many things may not be a problem per se, though it could lead to missing the right match (e.g. blah CHEC: something won't match "CHEC" because it matches "blah CHEC"). But the details are up to you.

I would like to keep the regex as is.

Sure, the details of the regex don't bother me much as simplifying the code a bit - regardless of which characters you choose to match,

312 ↗(On Diff #488229)

Ah, got it - sure.

bchetioui updated this revision to Diff 488607.Jan 12 2023, 5:04 AM
bchetioui marked 2 inline comments as done.

Remove comment prefix derivation and hardcode prefixes instead.

Thinking about this more, I think that you are right and the added complexity of not hardcoding the comment prefixes may not be worth it.
I decided to go with "decide we do care, and hardcode (any comment marker|start-of-line), dropping the optional-ness and language detection".

Thanks a lot again for your review, Sam!

llvm/utils/filecheck_lint/filecheck_lint/filecheck_lint.py
131 ↗(On Diff #488229)

Small note: the code path is not actually dead, as files with extensions that are not mapped to an inline comment prefix will have their directive_prefix set to None.

However, I take your point, and I am no longer sure if this is a net positive to add. In the worst case, if people start using the linter with new file formats that use different comment prefixes than the ones we match, they can always update the regex.

I deleted the code and hardcoded the inline comment prefixes in the regex.

(e.g. blah CHEC: something won't match "CHEC" because it matches "blah CHEC")

This can surely be improved, and I plan to do so in a later change.

sammccall accepted this revision.Jan 12 2023, 5:15 AM

LG, thanks!

llvm/utils/filecheck_lint/filecheck_lint/filecheck_lint.py
131 ↗(On Diff #488229)

the code path is not actually dead, as files with extensions that are not mapped to an inline comment prefix will have their directive_prefix set to None

The previous snapshot still had comment_prefix = comment_prefix or '//' - maybe an oversight and I was too eager to assume it was intentional!

This revision is now accepted and ready to land.Jan 12 2023, 5:15 AM
bchetioui marked an inline comment as done.Jan 12 2023, 5:17 AM
bchetioui added inline comments.
llvm/utils/filecheck_lint/filecheck_lint/filecheck_lint.py
131 ↗(On Diff #488229)

Oops, you're right, that was an oversight (I removed it now, and didn't even register it). Thanks for catching it, my bad!

This revision was landed with ongoing or failed builds.Jan 12 2023, 5:20 AM
This revision was automatically updated to reflect the committed changes.
bchetioui marked an inline comment as done.