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

[Merged by Bors] - feat: have/let linter #12190

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
aaa9d84
feat: have/let linter
adomani Apr 16, 2024
30280a5
add imports
adomani Apr 16, 2024
e25f2e2
Eric's MetaM run
adomani Apr 17, 2024
09cd667
Add a period to the linter message
adomani Apr 17, 2024
5bbe4aa
lint multiple introduced hypotheses
adomani Apr 17, 2024
98dbefc
haveLet linter only complains with noise
adomani Apr 17, 2024
7a4e089
import at bottom and shake
adomani Apr 17, 2024
8dbc7fd
Remove exclusions
adomani Apr 19, 2024
0f74c28
linter option is `Nat` instead of `Bool`
adomani Apr 19, 2024
27bc724
reduce imports
adomani Apr 19, 2024
8a9dbec
module docs
adomani Apr 19, 2024
159c6e3
Merge branch 'master' into adomani/have_let_linter_only
adomani May 9, 2024
c7566c4
Update HaveLetLinter.lean
adomani May 9, 2024
3345288
silence test
adomani May 9, 2024
d7241ab
Merge remote-tracking branch 'origin/master' into adomani/have_let_li…
adomani May 20, 2024
160e2b8
Merge remote-tracking branch 'origin/master' into adomani/have_let_li…
adomani May 26, 2024
10937ef
move linter to Linter dir
adomani May 26, 2024
9724831
Merge remote-tracking branch 'origin/master' into adomani/have_let_li…
adomani Jun 17, 2024
06a9d69
unreported, reduce imports
adomani Jun 17, 2024
4bd7535
import linter in linter.lean
adomani Jun 17, 2024
a085a6b
Merge branch 'master' into adomani/have_let_linter_only
adomani Aug 6, 2024
c23b907
Jon's comments from review, reduce imports, logging option
adomani Aug 6, 2024
4c359f2
update tests
adomani Aug 6, 2024
d99a922
Apply suggestions from code review
adomani Aug 6, 2024
94894a9
Merge branch 'adomani/have_let_linter_only' of github.com:leanprover-…
adomani Aug 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Mathlib.lean
Original file line number Diff line number Diff line change
Expand Up @@ -3627,6 +3627,7 @@ import Mathlib.Tactic.GuardGoalNums
import Mathlib.Tactic.GuardHypNums
import Mathlib.Tactic.Have
import Mathlib.Tactic.HaveI
import Mathlib.Tactic.HaveLetLinter
import Mathlib.Tactic.HelpCmd
import Mathlib.Tactic.HigherOrder
import Mathlib.Tactic.Hint
Expand Down
1 change: 1 addition & 0 deletions Mathlib/Tactic.lean
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import Mathlib.Tactic.GuardGoalNums
import Mathlib.Tactic.GuardHypNums
import Mathlib.Tactic.Have
import Mathlib.Tactic.HaveI
import Mathlib.Tactic.HaveLetLinter
import Mathlib.Tactic.HelpCmd
import Mathlib.Tactic.HigherOrder
import Mathlib.Tactic.Hint
Expand Down
99 changes: 99 additions & 0 deletions Mathlib/Tactic/HaveLetLinter.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/-
Copyright (c) 2024 Damiano Testa. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Damiano Testa
-/
import Lean.Elab.Command
import Lean.Linter.Util
import Std.Data.List.Basic

/-!
# The `have` vs `let` linter

The `have` vs `let` linter flags uses of `have` to introduce a hypothesis whose Type is not `Prop`.

TODO:
* `replace` implies `have`: should it be ignored?
* `haveI` may need to change to `let/letI`?
* also do `let` vs `have`.
-/

open Lean Elab Command Meta

namespace Mathlib.Linter

/-- The `have` vs `let` linter emits a warning on `have`s introducing a hypothesis whose
Type is not `Prop`. -/
register_option linter.haveLet : Bool := {
defValue := true
descr := "enable the `have` vs `let` linter"
}

namespace haveLet

/-- find the `have` syntax. -/
partial
def isHave? : Syntax → Bool
| .node _ ``Lean.Parser.Tactic.tacticHave_ _ => true
|_ => false

/-- `SyntaxNodeKind`s that imply a `have` but should be ignored anyway. -/
abbrev exclusions : HashSet SyntaxNodeKind := HashSet.empty
|>.insert ``Lean.Parser.Tactic.replace
|>.insert `Std.Tactic.classical!
|>.insert `Mathlib.Tactic.Tauto.tauto

end haveLet

end Mathlib.Linter

namespace Mathlib.Linter.haveLet

/-- returns the `have` syntax whose corresponding hypothesis does not have Type `Prop` and
also a `Format`ted version of the corresponding Type. -/
partial
def nonPropHaves : InfoTree → CommandElabM (Array (Syntax × Format))
| .node i args => do
let nargs := (← args.toArray.mapM nonPropHaves).flatten
if let .ofTacticInfo i := i then
let stx := i.stx
if exclusions.contains stx.getKind then return #[] else
if isHave? stx then
let mctx := i.mctxAfter
let mvdecls := (i.goalsAfter.map (mctx.decls.find? ·)).reduceOption
let _ : Ord MetavarDecl := { compare := (compare ·.index ·.index) }
-- we extract the `MetavarDecl` with largest index after a `have`, since this one
-- holds information about the metavariable where `have` introduces the new hypothesis.
let largestIdx := mvdecls.toArray.qsort (·.index > ·.index)
-- the relevant `LocalContext`
let lc := (largestIdx.getD 0 default).lctx
-- and the last declaration introduced: the one that `have` created
let ld := (lc.lastDecl.getD default).type
-- now, we get the `MetaM` state up and running to find the type of `ld`
let res ← liftCoreM do MetaM.run (ctx := { lctx := lc }) (s := { mctx := mctx }) <| do
adomani marked this conversation as resolved.
Show resolved Hide resolved
let typ ← inferType (← instantiateMVars ld)
if ! typ.isProp then
return nargs.push (stx, ← ppExpr ld)
else return nargs
return res.1
else return nargs
else return nargs
adomani marked this conversation as resolved.
Show resolved Hide resolved
| .context _ t => nonPropHaves t
| .hole _ => return #[]

/-- Gets the value of the `linter.haveLet` option. -/
def getLinterHash (o : Options) : Bool := Linter.getLinterValue linter.haveLet o

/-- The main implementation of the `have` vs `let` linter. -/
def haveLetLinter : Linter where run := withSetOptionIn fun _stx => do
unless getLinterHash (← getOptions) && (← getInfoState).enabled do
return
if (← MonadState.get).messages.hasErrors then
return
let trees ← getInfoTrees
for t in trees.toArray do
for (s, fmt) in ← nonPropHaves t do
Linter.logLint linter.haveLet s m!"'{fmt}' is a Type and not a Prop. \
Consider using 'let' instead of 'have'"

initialize addLinter haveLetLinter
73 changes: 73 additions & 0 deletions test/HaveLetLinter.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Mathlib.Tactic.HaveLetLinter
import Mathlib.Tactic.Tauto

#guard_msgs in
-- check that `tauto` does not trigger the linter
example : True := by
tauto

#guard_msgs in
-- replace is ignored, no matter what
example : True := by
let zero := 0
replace _zero := zero
let eq := (rfl : 0 = 0)
replace _eq := eq
exact .intro

set_option linter.haveLet false in
set_option linter.haveLet true in
/--
warning: 'Nat' is a Type and not a Prop. Consider using 'let' instead of 'have' [linter.haveLet]
---
warning: 'Nat' is a Type and not a Prop. Consider using 'let' instead of 'have' [linter.haveLet]
---
warning: 'Nat' is a Type and not a Prop. Consider using 'let' instead of 'have' [linter.haveLet]
---
warning: 'Nat' is a Type and not a Prop. Consider using 'let' instead of 'have' [linter.haveLet]
-/
#guard_msgs in
example : True := by
have _a := 0
have _b : Nat := 0
have _b : 0 = 0 := rfl
have _oh : Nat := 0
have _b : Nat := 2
tauto
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a way to guard an unfinished proof, like this one without tauto? If I understand your motivation for level 1 correctly, it's also to trigger on unfinished proofs, isn't it?

I expected that if I delete the #guard_msgs here and then delete tauto that the linter warnings would pop up. Did I understand this wrongly?

Copy link
Collaborator Author

@adomani adomani Aug 6, 2024

Choose a reason for hiding this comment

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

The linter emits a warning if a declaration simultaneously does not have errors and yet emits messages. Simply removing tauto leaves an unsolved goals error, so the linter shuts up. If, instead, you replace tauto with sorry, then the linter will tell you that you should use let/have.

I could make the linter chatty also in the presence of errors, but I think that on an erroring declaration, the linter will yield dubious reports and would be simply extra noise.

Copy link
Collaborator

Choose a reason for hiding this comment

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

ok. I somehow expect that an "unsolved goals" error behaves very similar to a "declaration uses sorry" warning in as many aspects as possible but I see that's not as easy with how messages are reported.

Looks good to me as is.


set_option linter.haveLet false in
set_option linter.haveLet true in
/--
warning: 'Nat' is a Type and not a Prop. Consider using 'let' instead of 'have' [linter.haveLet]
-/
#guard_msgs in
example : True := by
have := Nat.succ ?_;
exact .intro
exact 0

#guard_msgs in
example : True := by
have := And.intro (Nat.add_comm ?_ ?_) (Nat.add_comm ?_ ?_)
apply True.intro
exact 0
exact 0
exact 0
exact 0

#guard_msgs in
example (h : False) : True := by
have : False := h
exact .intro

-- (← `(tactic| have := 0))

set_option linter.haveLet false in
set_option linter.haveLet true in
/--
warning: 'Nat' is a Type and not a Prop. Consider using 'let' instead of 'have' [linter.haveLet]
-/
#guard_msgs in
theorem ghi : True := by
have : Nat := Nat.succ 1;
exact .intro
Loading