Skip to content

Commit

Permalink
#URL macro (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
mallexxx authored Mar 6, 2024
1 parent e3dc4fa commit d51beaf
Show file tree
Hide file tree
Showing 10 changed files with 531 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
.DS_Store
xcuserdata/
.vscode
.swiftpm/
82 changes: 82 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
allow_zero_lintable_files: true

disabled_rules:
- no_space_in_method_call
- multiple_closures_with_trailing_closure
- block_based_kvo
- compiler_protocol_init
- unused_setter_value
- line_length
- type_name
- implicit_getter
- function_parameter_count
- trailing_comma
- nesting
- opening_brace

opt_in_rules:
- file_header
- explicit_init

custom_rules:
explicit_non_final_class:
included: ".*\\.swift"
name: "Implicitly non-final class"
regex: "^\\s*(class) (?!func|var)"
capture_group: 0
match_kinds:
- keyword
message: "Classes should be `final` by default, use explicit `internal` or `public` for non-final classes."
severity: error
enforce_os_log_wrapper:
included: ".*\\.swift"
name: "Use `import Common` for os_log instead of `import os.log`"
regex: "^(import (?:os\\.log|os|OSLog))$"
capture_group: 0
message: "os_log wrapper ensures log args are @autoclosures (computed when needed) and to be able to use String Interpolation."
severity: error

analyzer_rules: # Rules run by `swiftlint analyze`
- explicit_self

# Rule Config
identifier_name:
min_length: 1
max_length: 1000
file_length:
warning: 1200
error: 1200
type_body_length:
warning: 500
error: 500
large_tuple:
warning: 4
error: 5
file_header:
required_pattern: |
\/\/
\/\/ SWIFTLINT_CURRENT_FILENAME
\/\/
\/\/ Copyright © \d{4} DuckDuckGo\. All rights reserved\.
\/\/
\/\/ Licensed under the Apache License, Version 2\.0 \(the \"License\"\);
\/\/ you may not use this file except in compliance with the License\.
\/\/ You may obtain a copy of the License at
\/\/
\/\/ http:\/\/www\.apache\.org\/licenses\/LICENSE-2\.0
\/\/
\/\/ Unless required by applicable law or agreed to in writing, software
\/\/ distributed under the License is distributed on an \"AS IS\" BASIS,
\/\/ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.
\/\/ See the License for the specific language governing permissions and
\/\/ limitations under the License\.
\/\/
# General Config
excluded:
- Package.swift
- .build
- scripts/
- Sources/RemoteMessaging/Model/AnyDecodable.swift
- Sources/Common/Concurrency/AsyncStream.swift

32 changes: 32 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"pins" : [
{
"identity" : "swift-macro-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-macro-testing.git",
"state" : {
"revision" : "10dcef36314ddfea6f60442169b0b320204cbd35",
"version" : "0.2.2"
}
},
{
"identity" : "swift-snapshot-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : {
"revision" : "e7b77228b34057041374ebef00c0fd7739d71a2b",
"version" : "1.15.3"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "64889f0c732f210a935a0ad7cda38f77f876262d",
"version" : "509.1.1"
}
}
],
"version" : 2
}
32 changes: 30 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// swift-tools-version:5.7
// swift-tools-version:5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import CompilerPluginSupport
import PackageDescription
import Foundation

let package = Package(
name: "AppleToolbox",
Expand All @@ -12,8 +12,12 @@ let package = Package(
],
products: [
.plugin(name: "SwiftLintPlugin", targets: ["SwiftLintPlugin"]),
.library(name: "Macros", targets: ["Macros"]),
],
dependencies: [
// Depend on the Swift 5.9 release of SwiftSyntax
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
.package(url: "https://github.com/pointfreeco/swift-macro-testing.git", from: "0.2.2"),
],
targets: [
.plugin(
Expand All @@ -28,5 +32,29 @@ let package = Package(
url: "https://github.com/realm/SwiftLint/releases/download/0.54.0/SwiftLintBinary-macos.artifactbundle.zip",
checksum: "963121d6babf2bf5fd66a21ac9297e86d855cbc9d28322790646b88dceca00f1"
),
// Macro implementation that performs the source transformation of a macro.
.macro(
name: "MacrosImplementation",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
],
plugins: [.plugin(name: "SwiftLintPlugin")]
),
// Library that exposes a macro as part of its API, which is used in client programs.
.target(
name: "Macros",
dependencies: ["MacrosImplementation"],
plugins: [.plugin(name: "SwiftLintPlugin")]
),
.testTarget(
name: "MacrosTests",
dependencies: [
"MacrosImplementation",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
.product(name: "MacroTesting", package: "swift-macro-testing"),
],
plugins: [.plugin(name: "SwiftLintPlugin")]
),
]
)
37 changes: 37 additions & 0 deletions Sources/Macros/MacroDefinitions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// MacroDefinitions.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

/// Compile-time validated OS version independent URL instantiation from String Literal.
///
/// Used to statically validate preset URLs.
/// The idea is to disable any flaky URL conversions like punycode or no-scheme urls, only 1-1 mapped String Literals can be used.
///
/// Usage: `let url = #URL("https://duckduckgo.com")`
///
/// - Note: Strings like "http://💩.la" or "1" are not valid #URL parameter values.
/// To instantiate a parametrized URL use `URL.appendingPathComponent(_:)` or `URL.appendingParameters(_:allowedReservedCharacters:)`
/// To instantiate a URL from a String format, use `URL(string:)`
///
/// - Parameter string: valid URL String Literal with URL scheme
/// - Returns: URL instance if provided string argument is a valid URL
/// - Throws: Compile-time error if provided string argument is not a valid URL
///
@freestanding(expression)
public macro URL(_ string: StaticString) -> URL = #externalMacro(module: "MacrosImplementation", type: "URLMacro")
32 changes: 32 additions & 0 deletions Sources/MacrosImplementation/MacroError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// MacroError.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

enum MacroError: Error, CustomStringConvertible {

case message(String)

var description: String {
switch self {
case .message(let text):
return text
}
}

}
27 changes: 27 additions & 0 deletions Sources/MacrosImplementation/MacrosCompilerPlugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// MacrosCompilerPlugin.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main
struct MacrosCompilerPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
URLMacro.self,
]
}
65 changes: 65 additions & 0 deletions Sources/MacrosImplementation/URLMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// URLMacro.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import SwiftSyntax
import SwiftSyntaxMacros

/// Compile-time validated URL instantiation
/// - Returns: URL instance if provided string argument is a valid URL
/// - Throws: Compile-time error if provided string argument is not a valid URL
/// - Usage: `let url = #URL("https://duckduckgo.com")`
public struct URLMacro: ExpressionMacro {

static let invalidCharacters = CharacterSet.urlQueryAllowed
.union(CharacterSet(charactersIn: "%+?#[]"))
.inverted
static let urlSchemeAllowedCharacters = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: ".-"))

public static func expansion(of node: some SwiftSyntax.FreestandingMacroExpansionSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> SwiftSyntax.ExprSyntax {

guard node.argumentList.count == 1 else {
throw MacroError.message("#URL macro should have one String literal argument")
}
guard let literal = node.argumentList.first?.expression.as(StringLiteralExprSyntax.self),
literal.segments.count == 1,
let string = literal.segments.first?.as(StringSegmentSyntax.self)?.content else {
throw MacroError.message("#URL argument should be a String literal")
}

if let idx = string.text.rangeOfCharacter(from: Self.invalidCharacters)?.lowerBound {
throw MacroError.message("\"\(string.text)\" has invalid character at index \(string.text.distance(from: string.text.startIndex, to: idx)) (\(string.text[idx]))")
}
guard let scheme = string.text.range(of: ":").map({ string.text[..<$0.lowerBound] }),
scheme.rangeOfCharacter(from: Self.urlSchemeAllowedCharacters.inverted) == nil else {
throw MacroError.message("URL must contain a scheme")
}
guard let url = URL(string: string.text) else {
throw MacroError.message("\"\(string.text)\" is not a valid URL")
}
guard url.scheme == String(scheme) else {
throw MacroError.message("URL must contain a scheme")
}
guard url.absoluteString == string.text else {
throw MacroError.message("Resulting URL \"\(url.absoluteString)\" is not equal to \"\(string.text)\"")
}

return "URL(string: \"\(raw: string.text)\")!"
}

}
17 changes: 17 additions & 0 deletions Tests/.swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
disabled_rules:
- file_length
- unused_closure_parameter
- type_name
- force_cast
- force_try
- function_body_length
- cyclomatic_complexity
- identifier_name
- blanket_disable_command
- type_body_length
- explicit_non_final_class
- enforce_os_log_wrapper

large_tuple:
warning: 6
error: 10
Loading

0 comments on commit d51beaf

Please sign in to comment.