[PATCH v2] userdiff: add support for Swift

Shlok Kulshreshtha <[email protected]>
Newsgroups org.kernel.vger.git
Message-ID <[email protected]>
Add a built-in userdiff driver for the Swift programming language so that
diff hunk headers and word diffs work out of the box for ".swift" files.

The funcname pattern is built for Swift's own declaration grammar: an
optional run of attributes ("@objc", "@available(iOS 13, *)", ...),
followed by an optional run of lowercase modifiers ("public", "static",
"final", ...), followed by a declaration keyword (func, class, struct,
enum, protocol, extension, actor, init, deinit, subscript). The keyword
is followed by a boundary that allows whitespace, "(" (init/subscript),
"?" or "!" (failable init), or "<" (generics), while still acting as a
word boundary so e.g. "initialize(" does not match.

The word regex recognizes Swift identifiers, hexadecimal, octal, binary,
integer and floating-point literals, and the language's operators.

Signed-off-by: Shlok Kulshreshtha <[email protected]>
---
v2, addressing Johannes Sixt's review of v1
(<[email protected]>).  Changes since v1:

 - t4018/swift-{init,failable-init,generic-subscript}: "RIGHT" now
   appears only once, on the declaration line, so the expected header is
   unambiguous.
 - word regex: dropped the redundant "?" after the single-character
   operator class.  Single characters are already covered by the
   "|[^[:space:]]" fallback that the PATTERNS macro appends, so only the
   two-character forms need to be spelled out.

(A couple of Hannes's other suggestions I kept as-is; I have explained
the reasoning in a reply to his review.)

Some coverage evidence beyond the t4018 fixtures:

 - Grammar: a test over every declaration form in Swift's grammar
   summary (26 forms -- func/class/struct/enum/protocol/extension/actor,
   init incl. "init?"/"init!"/generic, deinit, subscript incl. generic,
   operator methods, stacked modifiers, inline attributes with and
   without arguments, "where" clauses, multi-line signatures) -- all 26
   resolve to the correct declaration.

 - Corpus: run over the last 200 commits touching *.swift in seven
   stylistically different projects (Alamofire, apple/
   swift-argument-parser, vapor, Kingfisher, RxSwift, SnapKit,
   pointfreeco/swift-composable-architecture): of 20454 hunks, 15310
   produced a header and 15296 (99.9%) named a real declaration.  The
   empty-header hunks are changes with no enclosing declaration (file
   comment blocks, imports, Package.swift, top-level code); sampling
   found no change inside a declaration that failed to get a header.
   The handful of non-declaration headers are the selective-import form
   ("import class Foundation.Bundle"), which reads "import" as a
   modifier; rare and low-harm, and I can exclude it in a follow-up if
   preferred.

 Documentation/gitattributes.adoc  |  2 ++
 t/t4018/swift-actor               |  5 +++++
 t/t4018/swift-attribute-with-args |  7 +++++++
 t/t4018/swift-class               |  5 +++++
 t/t4018/swift-enum                |  5 +++++
 t/t4018/swift-extension           |  5 +++++
 t/t4018/swift-failable-init       |  7 +++++++
 t/t4018/swift-func                |  5 +++++
 t/t4018/swift-generic-subscript   |  7 +++++++
 t/t4018/swift-init                |  7 +++++++
 t/t4018/swift-inline-attribute    |  7 +++++++
 t/t4018/swift-modifiers           |  4 ++++
 t/t4018/swift-protocol            |  5 +++++
 t/t4018/swift-struct              |  5 +++++
 userdiff.c                        | 10 ++++++++++
 15 files changed, 86 insertions(+)
 create mode 100644 t/t4018/swift-actor
 create mode 100644 t/t4018/swift-attribute-with-args
 create mode 100644 t/t4018/swift-class
 create mode 100644 t/t4018/swift-enum
 create mode 100644 t/t4018/swift-extension
 create mode 100644 t/t4018/swift-failable-init
 create mode 100644 t/t4018/swift-func
 create mode 100644 t/t4018/swift-generic-subscript
 create mode 100644 t/t4018/swift-init
 create mode 100644 t/t4018/swift-inline-attribute
 create mode 100644 t/t4018/swift-modifiers
 create mode 100644 t/t4018/swift-protocol
 create mode 100644 t/t4018/swift-struct

diff --git a/Documentation/gitattributes.adoc b/Documentation/gitattributes.adoc
index bd76167a45..9fea75f96f 100644
--- a/Documentation/gitattributes.adoc
+++ b/Documentation/gitattributes.adoc
@@ -914,6 +914,8 @@ patterns are available:
 - `scheme` suitable for source code in most Lisp dialects,
   including Scheme, Emacs Lisp, Common Lisp, and Clojure.
 
+- `swift` suitable for source code in the Swift language.
+
 - `tex` suitable for source code for LaTeX documents.
 
 
diff --git a/t/t4018/swift-actor b/t/t4018/swift-actor
new file mode 100644
index 0000000000..e4852f40a7
--- /dev/null
+++ b/t/t4018/swift-actor
@@ -0,0 +1,5 @@
+actor RIGHT {
+    let a = 1
+    // a comment
+    let b = ChangeMe
+}
diff --git a/t/t4018/swift-attribute-with-args b/t/t4018/swift-attribute-with-args
new file mode 100644
index 0000000000..22b1ee32f1
--- /dev/null
+++ b/t/t4018/swift-attribute-with-args
@@ -0,0 +1,7 @@
+struct View {
+    @available(iOS 13, *) public func RIGHT() {
+        let a = 1
+        // a comment
+        print(ChangeMe)
+    }
+}
diff --git a/t/t4018/swift-class b/t/t4018/swift-class
new file mode 100644
index 0000000000..c3a9336027
--- /dev/null
+++ b/t/t4018/swift-class
@@ -0,0 +1,5 @@
+class RIGHT {
+    let a = 1
+    // a comment
+    let b = ChangeMe
+}
diff --git a/t/t4018/swift-enum b/t/t4018/swift-enum
new file mode 100644
index 0000000000..0a84302993
--- /dev/null
+++ b/t/t4018/swift-enum
@@ -0,0 +1,5 @@
+enum RIGHT {
+    case first
+    // a comment
+    case ChangeMe
+}
diff --git a/t/t4018/swift-extension b/t/t4018/swift-extension
new file mode 100644
index 0000000000..cbc18ab6ef
--- /dev/null
+++ b/t/t4018/swift-extension
@@ -0,0 +1,5 @@
+extension RIGHT {
+    static let a = 1
+    // a comment
+    static let b = ChangeMe
+}
diff --git a/t/t4018/swift-failable-init b/t/t4018/swift-failable-init
new file mode 100644
index 0000000000..4bbd6217c9
--- /dev/null
+++ b/t/t4018/swift-failable-init
@@ -0,0 +1,7 @@
+class Bar {
+    init?(RIGHT: Int) {
+        let x = 0
+        // a comment
+        print(ChangeMe)
+    }
+}
diff --git a/t/t4018/swift-func b/t/t4018/swift-func
new file mode 100644
index 0000000000..1fecae0911
--- /dev/null
+++ b/t/t4018/swift-func
@@ -0,0 +1,5 @@
+func RIGHT(x: Int) -> Int {
+    let y = x
+    // a comment
+    return ChangeMe
+}
diff --git a/t/t4018/swift-generic-subscript b/t/t4018/swift-generic-subscript
new file mode 100644
index 0000000000..423cb58941
--- /dev/null
+++ b/t/t4018/swift-generic-subscript
@@ -0,0 +1,7 @@
+struct Container {
+    subscript<RIGHT>(index: Int) -> Int {
+        let a = 0
+        // a comment
+        return ChangeMe
+    }
+}
diff --git a/t/t4018/swift-init b/t/t4018/swift-init
new file mode 100644
index 0000000000..dc7a298f38
--- /dev/null
+++ b/t/t4018/swift-init
@@ -0,0 +1,7 @@
+class Foo {
+    init(RIGHT: Int) {
+        let x = 0
+        // a comment
+        print(ChangeMe)
+    }
+}
diff --git a/t/t4018/swift-inline-attribute b/t/t4018/swift-inline-attribute
new file mode 100644
index 0000000000..2374c4b603
--- /dev/null
+++ b/t/t4018/swift-inline-attribute
@@ -0,0 +1,7 @@
+class Service {
+    @objc func RIGHT() {
+        let path = "/api"
+        // a comment
+        log(ChangeMe)
+    }
+}
diff --git a/t/t4018/swift-modifiers b/t/t4018/swift-modifiers
new file mode 100644
index 0000000000..9d80685a78
--- /dev/null
+++ b/t/t4018/swift-modifiers
@@ -0,0 +1,4 @@
+public static func RIGHT() -> Int {
+    // a comment
+    return ChangeMe
+}
diff --git a/t/t4018/swift-protocol b/t/t4018/swift-protocol
new file mode 100644
index 0000000000..07c39ec2a3
--- /dev/null
+++ b/t/t4018/swift-protocol
@@ -0,0 +1,5 @@
+protocol RIGHT {
+    var first: Int { get }
+    // a comment
+    var second: ChangeMe { get }
+}
diff --git a/t/t4018/swift-struct b/t/t4018/swift-struct
new file mode 100644
index 0000000000..e399ed7759
--- /dev/null
+++ b/t/t4018/swift-struct
@@ -0,0 +1,5 @@
+struct RIGHT {
+    let a = 1
+    // a comment
+    let b = ChangeMe
+}
diff --git a/userdiff.c b/userdiff.c
index b5412e6bc3..7129bf1482 100644
--- a/userdiff.c
+++ b/userdiff.c
@@ -362,6 +362,16 @@ PATTERNS("scheme",
 	 "\\|([^|\\\\]|\\\\.)*\\|"
 	 /* All other words should be delimited by spaces or parentheses. */
 	 "|([^][)(}{ \t])+"),
+PATTERNS("swift",
+	 "^[ \t]*((@[A-Za-z_][A-Za-z0-9_]*(\\([^()]*\\))?[ \t]+)*([a-z]+[ \t]+)*(func|init|deinit|subscript|class|struct|enum|protocol|extension|actor)[ \t(?!<].*)$",
+	 /* -- */
+	 "[a-zA-Z_][a-zA-Z0-9_]*"
+	 /* hexadecimal, octal, and binary literals */
+	 "|0[xX][0-9a-fA-F_]+|0[oO][0-7_]+|0[bB][01_]+"
+	 /* integers and floating-point numbers */
+	 "|[0-9][0-9_]*([.][0-9_]+)?([eE][-+]?[0-9]+)?"
+	 /* unary and binary operators */
+	 "|[-+*/%<>=!&|^~?]=|&&|\\|\\||<<=?|>>=?|\\?\\?|\\.\\.[.<]|->"),
 PATTERNS("tex", "^(\\\\((sub)*section|chapter|part)\\*{0,1}\\{.*)$",
 	 "\\\\[a-zA-Z@]+|\\\\.|([a-zA-Z0-9]|[^\x01-\x7f])+"),
 { .name = "default", .binary = -1 },

Range-diff against v1:
1:  1e7e199355 ! 1:  af48611565 userdiff: add support for Swift
    @@ t/t4018/swift-failable-init (new)
     @@
     +class Bar {
     +    init?(RIGHT: Int) {
    -+        let value = RIGHT
    ++        let x = 0
     +        // a comment
     +        print(ChangeMe)
     +    }
    @@ t/t4018/swift-func (new)
      ## t/t4018/swift-generic-subscript (new) ##
     @@
     +struct Container {
    -+    subscript<RIGHT>(index: RIGHT) -> Int {
    ++    subscript<RIGHT>(index: Int) -> Int {
     +        let a = 0
     +        // a comment
     +        return ChangeMe
    @@ t/t4018/swift-init (new)
     @@
     +class Foo {
     +    init(RIGHT: Int) {
    -+        let value = RIGHT
    ++        let x = 0
     +        // a comment
     +        print(ChangeMe)
     +    }
    @@ userdiff.c: PATTERNS("scheme",
     +	 /* integers and floating-point numbers */
     +	 "|[0-9][0-9_]*([.][0-9_]+)?([eE][-+]?[0-9]+)?"
     +	 /* unary and binary operators */
    -+	 "|[-+*/%<>=!&|^~?]=?|&&|\\|\\||<<=?|>>=?|\\?\\?|\\.\\.[.<]|->"),
    ++	 "|[-+*/%<>=!&|^~?]=|&&|\\|\\||<<=?|>>=?|\\?\\?|\\.\\.[.<]|->"),
      PATTERNS("tex", "^(\\\\((sub)*section|chapter|part)\\*{0,1}\\{.*)$",
      	 "\\\\[a-zA-Z@]+|\\\\.|([a-zA-Z0-9]|[^\x01-\x7f])+"),
      { .name = "default", .binary = -1 },
-- 
2.52.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.