diff --git a/.buildkite/check_regex.sh b/.buildkite/check_regex.sh new file mode 100755 index 0000000..24816db --- /dev/null +++ b/.buildkite/check_regex.sh @@ -0,0 +1,9 @@ +#!/bin/bash -eu + +cd Tools + +echo '--- :swift: Build' +swift build --product RegexChecker + +echo '--- :face_with_monocle: Check Regular Expressions' +swift run --skip-build RegexChecker diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 743c722..628df5b 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -39,6 +39,15 @@ steps: env: *common_env plugins: *common_plugins + ################# + # Check Regex + ################# + - label: ":face_with_monocle: Check Regular Expressions" + key: "check-regex" + command: ".buildkite/check_regex.sh" + env: *common_env + plugins: *common_plugins + ################# # Publish the Podspec (if we're building a tag) ################# @@ -52,4 +61,5 @@ steps: - "test" - "validate" - "lint" + - "check-regex" if: build.tag != null diff --git a/.gitignore b/.gitignore index 6da8421..9ea2232 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ vendor/ fastlane/test_output fastlane/README.md fastlane/report.xml + +.swiftpm diff --git a/Tools/Package.resolved b/Tools/Package.resolved new file mode 100644 index 0000000..4a7547e --- /dev/null +++ b/Tools/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "72d3da66b085c2299dd287c2be3b92b5ebd226de", + "version" : "0.50700.1" + } + } + ], + "version" : 2 +} diff --git a/Tools/Package.swift b/Tools/Package.swift new file mode 100644 index 0000000..1283ab2 --- /dev/null +++ b/Tools/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 5.7 + +import PackageDescription + +let package = Package( + name: "Tools", + platforms: [.macOS(.v12)], + products: [ + .executable( + name: "RegexChecker", + targets: ["RegexChecker"]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-syntax.git", exact: "0.50700.1"), + ], + targets: [ + .executableTarget( + name: "RegexChecker", + dependencies: [ + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftSyntaxParser", package: "swift-syntax"), + ] + ) + ] +) diff --git a/Tools/Sources/RegexChecker/main.swift b/Tools/Sources/RegexChecker/main.swift new file mode 100644 index 0000000..dd9b422 --- /dev/null +++ b/Tools/Sources/RegexChecker/main.swift @@ -0,0 +1,95 @@ +import Foundation +import SwiftSyntax +import SwiftSyntaxParser + +class RegexCheckerVisitor: SyntaxVisitor { + var declaration: VariableDeclSyntax? + + var regexes = [String]() + var shouldFail: Bool = false + + override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { + for piece in (node.leadingTrivia ?? []) { + if case let .lineComment(comment) = piece, + comment.hasPrefix("// CHECK-REGEX") { + self.declaration = node + break + } + } + + return .visitChildren + } + + override func visitPost(_ node: VariableDeclSyntax) { + self.declaration = nil + } + + override func visit(_ literal: StringLiteralExprSyntax) -> SyntaxVisitorContinueKind { + guard declaration != nil else { + return .visitChildren + } + + + for line in declaration!.description.split(separator: "\n") { + print("> \(line.trimmingCharacters(in: .whitespaces))") + } + + guard literal.firstToken?.tokenKind == .rawStringDelimiter("#"), + literal.lastToken?.tokenKind == .rawStringDelimiter("#") else { + print("❌ Regex declaration must use raw string literal (i.e. #\"regex\"#).") + shouldFail = true + return .skipChildren + } + + var regex = literal.description + regex.removeFirst() + regex.removeLast() + + do { + let _ = try NSRegularExpression(pattern: regex) + print("✅ Valid regular expression.") + } catch { + print("❌ Not a regular expression.") + shouldFail = true + } + + return .skipChildren + } + + private func reportFailure(_ message: String) { + print("^ \(message)") + + shouldFail = true + } +} + +func checkRegexLiterals(directory: URL) throws -> Bool { + guard let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil) else { + fatalError("Can't enumerate directory at \(directory)") + } + + var hasInvalidRegex = false + + for case let file as URL in enumerator { + guard file.lastPathComponent.hasSuffix(".swift") else { continue } + + let visitor = RegexCheckerVisitor() + let syntax = try SyntaxParser.parse(file) + visitor.walk(syntax) + + if visitor.shouldFail { + hasInvalidRegex = true + } + } + + return !hasInvalidRegex +} + +func main() throws { + let thisFile = URL(fileURLWithPath: #filePath) + let sourcesDirectory = URL(string: "../../../WordPressShared", relativeTo: thisFile)! + let success = try checkRegexLiterals(directory: sourcesDirectory) + exit(success ? 0 : 1) +} + +try main() diff --git a/WordPressShared/Core/Utility/String+StripGutenbergContentForExcerpt.swift b/WordPressShared/Core/Utility/String+StripGutenbergContentForExcerpt.swift index 6154922..31f6d96 100644 --- a/WordPressShared/Core/Utility/String+StripGutenbergContentForExcerpt.swift +++ b/WordPressShared/Core/Utility/String+StripGutenbergContentForExcerpt.swift @@ -14,7 +14,8 @@ extension String { /// Strips Gutenberg galleries from strings. /// private func strippingGutenbergGalleries() -> String { - let pattern = "(?s)" + // CHECK-REGEX + let pattern = #"(?s)"# return removingMatches(pattern: pattern, options: .caseInsensitive) } diff --git a/WordPressShared/Core/Utility/String+StripShortcodes.swift b/WordPressShared/Core/Utility/String+StripShortcodes.swift index 4b6e0f5..905bbd2 100644 --- a/WordPressShared/Core/Utility/String+StripShortcodes.swift +++ b/WordPressShared/Core/Utility/String+StripShortcodes.swift @@ -5,7 +5,8 @@ extension String { /// Creates a new string by stripping all shortcodes from this string. /// func strippingShortcodes() -> String { - let pattern = "\\[[^\\]]+\\]" + // CHECK-REGEX + let pattern = #"\[[^\]]+\]"# return removingMatches(pattern: pattern, options: .caseInsensitive) }