@@ -7,6 +7,7 @@ Authors: Michael Rothgang
77module
88
99import Mathlib.Init
10+ import Mathlib.Tactic.Linter.TextBased.UnicodeLinter
1011import Std.Internal.Parsec.String
1112
1213/-!
@@ -20,47 +21,56 @@ verify whether the title or body are written in present imperative tense.
2021open Std.Internal.Parsec String
2122
2223/-- Basic parser for PR titles: given a title `kind(scope): main title` or `kind: title`,
23- extracts the `kind` and `scope` components. In the future, this will be extended to also parse
24- the main PR title. -/
25- -- TODO: also parse and return the main PR title
26- def prTitle : Parser (String × Option String) :=
27- Prod.mk
28- <$> (["feat" , "chore" , "perf" , "refactor" , "style" , "fix" , "doc" , "test" , "ci" ].firstM pstring)
29- <*> (
24+ extracts the `kind`, `scope` and `main title` components. -/
25+ def prTitle : Parser (String × Option String × String) := do
26+ let kind ←
27+ ["feat" , "chore" , "perf" , "refactor" , "style" , "fix" , "doc" , "test" , "ci" ].firstM pstring
28+ let scope ← (
3029 (skipString "(" *> some <$> manyChars (notFollowedBy (skipString "):" ) *> any)
31- <* skipString "): " )
32- <|> (skipString ": " *> pure none)
30+ <* skipString "):" <* ws )
31+ <|> (skipString ":" *> ws *> pure none)
3332 )
33+ let mainTitle ← manyChars any
34+ return (kind, scope, mainTitle)
3435
3536-- Some self-tests for the parser.
36- /-- info: Except.ok ("feat", some "x") -/
37+
38+ /-- info: Except.ok ("feat", some "x", "") -/
39+ #guard_msgs in
40+ #eval Parser.run prTitle "feat(x):"
41+ /-- info: Except.ok ("feat", some "x", "") -/
42+ #guard_msgs in
43+ #eval Parser.run prTitle "feat(x): "
44+
45+ /-- info: Except.ok ("feat", some "x", "foo") -/
3746#guard_msgs in
3847#eval Parser.run prTitle "feat(x): foo"
39- /-- info: Except.ok ("feat", none) -/
48+ /-- info: Except.ok ("feat", none, "foo" ) -/
4049#guard_msgs in
4150#eval Parser.run prTitle "feat: foo"
42- /-- info: Except.error "offset 10: expected: ): " -/
51+ /-- info: Except.error "offset 10: expected: ):" -/
4352#guard_msgs in
4453#eval Parser.run prTitle "feat(: foo"
45- /-- info: Except.error "offset 4: expected: : " -/
54+ /-- info: Except.error "offset 4: expected: :" -/
4655#guard_msgs in
4756#eval Parser.run prTitle "feat): foo"
48- /-- info: Except.error "offset 4: expected: : " -/
57+ /-- info: Except.error "offset 4: expected: :" -/
4958#guard_msgs in
5059#eval Parser.run prTitle "feat)(: foo"
51- /-- info: Except.error "offset 4: expected: : " -/
60+ /-- info: Except.error "offset 4: expected: :" -/
5261#guard_msgs in
5362#eval Parser.run prTitle "feat)(sdf): foo"
54- /-- info: Except.ok ("feat", some "sdf") -/
63+ /-- info: Except.ok ("feat", some "sdf", "foo:" ) -/
5564#guard_msgs in
5665#eval Parser.run prTitle "feat(sdf): foo:"
57- /-- info: Except.error "offset 4: expected: : " -/
66+ /-- info: Except.error "offset 4: expected: :" -/
5867#guard_msgs in
5968#eval Parser.run prTitle "feat foo"
60- /-- info: Except.ok ("chore", none) -/
69+ /-- info: Except.ok ("chore", none, "test" ) -/
6170#guard_msgs in
6271#eval Parser.run prTitle "chore: test"
6372
73+ open Mathlib.Linter.TextBased in
6474/--
6575Check if `title` matches the mathlib conventions for PR titles
6676(documented at <https://leanprover-community.github.io/contribute/commit.html>).
@@ -70,8 +80,8 @@ are written in present imperative tense.
7080Return all error messages for violations found.
7181-/
7282public def validateTitle (title : String) : Array String := Id.run do
73- -- The title should be of the form "abbrev: main title " or "abbrev(scope): main title ".
74- -- We use the parser above to extract abbrev and scope ignoring the main title ,
83+ -- The title should be of the form "abbrev: subject " or "abbrev(scope): subject ".
84+ -- We use the parser above to extract abbrev, scope and PR subject ,
7585 -- but give some custom errors in some easily detectable cases.
7686 if !title.contains ':' then
7787 return #["error: the PR title does not contain a colon" ]
@@ -82,13 +92,47 @@ public def validateTitle (title : String) : Array String := Id.run do
8292 let knownKinds := ["feat" , "chore" , "perf" , "refactor" , "style" , "fix" , "doc" , "test" , "ci" ]
8393 match Parser.run prTitle title.trimAsciiStart.copy with
8494 | Except.error _ =>
85- return errors.push s! "error: the PR title should be of the form\n kind: main title\n \
86- or\n kind(scope): main title\n Allowed values for `kind` are { knownKinds} "
87- | Except.ok (_kind, _scope?) =>
88- -- Future: also check scope (and the main PR title)
95+ return errors.push s! "error: the PR title should be of the form\n kind: subject\n \
96+ or\n kind(scope): subject\n Allowed values for `kind` are { knownKinds} "
97+ | Except.ok (_kind, scope?, subject) =>
98+ if subject.isEmpty then
99+ errors := errors.push s! "error: the PR title should not be empty"
100+ if let some scope := scope? then
101+ if scope.startsWith "Mathlib/" then
102+ errors := errors.push s! "error: a PR's scope must not start with 'Mathlib/'"
103+ if scope.contains " " then
104+ errors := errors.push s! "error: a PR's scope must not contain spaces"
105+ if scope.contains "\\ " then
106+ errors := errors.push
107+ s! "error: a PR's scope must not contain backslashes --- use forward slashes instead"
108+ if scope.endsWith ".lean" then
109+ errors := errors.push s! "error: a PR's scope must not end with '.lean'"
110+ else if scope.contains '.' then
111+ errors := errors.push s! "error: a PR's scope should be a directory, not a module"
112+ -- Future: we could check if `scope` describes a directory that actually exist.
113+ -- Should we allow special syntax such as `Data/*/Basic` or `{Set,Group}Theory`?
114+
115+ -- Titles should be lower-cased (but we allow abbreviations).
116+ if subject.front.toLower != subject.front then
117+ let firstWord := subject.takeWhile (!·.isWhitespace)
118+ if !(firstWord.all (·.isUpper)) then
119+ errors := errors.push "error: the PR subject should be lowercased"
120+ if subject.endsWith "." then
121+ errors := errors.push "error: the PR title should not end with a full stop"
122+ else if subject.endsWith " " then
123+ errors := errors.push "error: the PR title should not end with a space"
89124 if title.contains " " then
90125 errors := errors.push
91126 "error: the PR title contains multiple consecutive spaces; please add just one"
92- if title.endsWith "." then
93- errors := errors.push "error: the PR title should not end with a full stop"
127+ if title.contains "\t " then
128+ errors := errors.push
129+ "error: the PR title contains a tab; please use single spaces instead"
130+ -- Check for unicode characters which are not allowed: we don't want direction-changing
131+ -- characters, invisible spaces or so (for example). We re-use the code in the Unicode linter.
132+ let badChars := title.chars.filter (fun c ↦ !UnicodeLinter.isAllowedCharacter c && c != '\t ' )
133+ if !badChars.isEmpty then
134+ let err := ", " .intercalate <| badChars.map
135+ (fun c ↦ s! "'{ c} ' ({ UnicodeLinter.Char.printCodepointHex c} )." )|>.toList
136+ errors := errors.push s! "error: the PR contains { badChars.length} Unicode characters \
137+ which are not allowed: { err} "
94138 return errors
0 commit comments