Skip to content

Instantly share code, notes, and snippets.

@maximkrouk
Last active May 30, 2020 14:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save maximkrouk/fcd6d2f8b9f633c1062ff90ba2e90338 to your computer and use it in GitHub Desktop.
Save maximkrouk/fcd6d2f8b9f633c1062ff90ba2e90338 to your computer and use it in GitHub Desktop.
Swift function parameter scanner
import Foundation
func extractParametersString(from functionString: String) -> String? {
var buffer = String(functionString.reversed())
if let returnSignIndex = buffer.range(of: "->")?.upperBound {
buffer = String(buffer[returnSignIndex...])
}
if let closingBraceIndex = buffer.range(of: ")")?.upperBound {
buffer = String(buffer[buffer.index(before: closingBraceIndex)...])
} else { return nil }
buffer = String(buffer.reversed())
if let funcKeywordIndex = buffer.range(of: "func")?.upperBound {
buffer = String(buffer[buffer.index(after: funcKeywordIndex)...])
} else { return nil }
buffer = String(buffer.drop(while: { $0.isLetter || $0.isNumber }))
if buffer.first == "(" { return buffer }
print("Generic functions are not yet supported")
return nil
}
struct FunctionParameter: Equatable {
var functionBuilder: String? = nil
var label: String? = nil
var name: String = ""
var type: String = ""
var defaultValue: String? = nil
func renderCallSite(value: String? = nil) -> String {
let _label = label.map { $0 != "_" ? "\($0): " : "" } ?? name
let _value = value ?? name
return _label
.appending(_value)
}
func render() -> String {
let _builder = functionBuilder.map { "@\($0) " } ?? ""
let _label = label.map { "\($0) " } ?? ""
let _defaultValue = defaultValue.map { " = \($0)" } ?? ""
return _builder
.appending(_label)
.appending(name)
.appending(": ")
.appending(type)
.appending(_defaultValue)
}
}
extension Array where Element == FunctionParameter {
func renderCallSite() -> String { "(" + map { $0.renderCallSite() }.joined(separator: ", ") + ")" }
func render() -> String { "(" + map { $0.render() }.joined(separator: ", ") + ")" }
}
class FunctionParameterScanner {
private var string: String
private var currentIndex: String.Index
private var isAtEnd: Bool { currentIndex == string.endIndex }
private var unscanned: Substring { string[currentIndex...] }
struct ParsingError: Error {
var message: String
var function: String
var file: String
var line: Int
init(
_ message: String = "",
function: String = #function,
file: String = #file,
line: Int = #line
) {
self.message = message
self.function = function
self.file = file
self.line = line
}
init(never: Never) { fatalError() }
var localizedDescription: String { message }
var debugDescription: String {
var output = ""
dump(self, to: &output)
return output
}
}
init(_ string: String) {
self.string = string
self.currentIndex = string.startIndex
}
func reload(_ string: String) {
self.string = string
}
func scan() throws -> [FunctionParameter] {
guard scanCharacter() == "(" else { throw ParsingError() }
var output: [FunctionParameter] = []
while !isAtEnd { try scanNextParameter(into: &output)}
return output
}
private func scanNextParameter(into buffer: inout [FunctionParameter]) throws {
if string[currentIndex] == ")" {
_ = scanCharacter()
return
}
var parameter = FunctionParameter()
try scanToType(into: &parameter)
try scanTypeAndDefaultValue(into: &parameter)
buffer.append(parameter)
}
private func scanToType(into parameter: inout FunctionParameter) throws {
guard var firstChunk = scanToCharacter(":")?.components(separatedBy: .whitespaces)
else { throw ParsingError() }
if
let firstItem = firstChunk.first,
let firstSymbol = firstItem.trimmingCharacters(in: .whitespaces).first,
firstSymbol == "@"
{
parameter.functionBuilder = String(firstItem.dropFirst())
firstChunk.removeFirst()
}
if firstChunk.count == 1 {
parameter.name = firstChunk[0]
} else if firstChunk.count == 2 {
parameter.label = firstChunk[0]
parameter.name = firstChunk[1]
} else {
throw ParsingError()
}
guard scanCharacters(from: [":", " "]) != nil else { throw ParsingError() }
}
private func scanTypeAndDefaultValue(into parameter: inout FunctionParameter) throws {
guard var new = scanToCharacters(from: [")", ",", "="]) else { throw ParsingError() }
if string[currentIndex] == "=" {
parameter.type = new.trimmingCharacters(in: .whitespaces)
guard scanCharacters(from: ["=", " "]) != nil else { throw ParsingError() }
try scanDefaultValue(into: &parameter)
} else if string[currentIndex] == "," {
parameter.type = new.trimmingCharacters(in: .whitespaces)
guard scanCharacters(from: [",", " "]) != nil else { throw ParsingError() }
} else if string[currentIndex] == ")" {
if isBracesCountEqualNonEmpty(in: new) {
parameter.type = new.trimmingCharacters(in: .whitespaces)
return
} else {
try scanAbnormalStuff(into: &new)
parameter.type = new.trimmingCharacters(in: .whitespaces)
if !isAtEnd, string[currentIndex] == "=" {
_ = scanCharacter()
try scanDefaultValue(into: &parameter)
}
}
} else {
throw ParsingError()
}
if isAtEnd { return }
if string[currentIndex] == "," { _ = scanCharacter() }
_ = scanCharacters(from: [" "])
}
private func bracesCount(in string: String) -> (open: Int, close: Int) {
let bracesCount = string.countOccurances(of: ["(", ")"])
return (bracesCount["("]!, bracesCount[")"]!)
}
private func isBracesCountEqualNonEmpty(in string: String) -> Bool {
let count = bracesCount(in: string)
return count.open == count.close && count.open > 0
}
private func isBracesCountEqual(in string: String) -> Bool {
let count = bracesCount(in: string)
return count.open == count.close
}
private func scanAbnormalStuff(into buffer: inout String) throws {
func _scanAbnormalStuff(into buffer: inout String) throws {
if isBracesCountEqualNonEmpty(in: buffer) && [",", ")", "="].contains(string[currentIndex]) { return }
guard let new = scanToCharacters(from: [")", ",", " "])
else { throw ParsingError() }
buffer.append(new)
if string[currentIndex] == "," {
if isBracesCountEqual(in: buffer) { return }
}
buffer.append(scanCharacter()!)
if isAtEnd {
let bracesCount = buffer.countOccurances(of: ["(", ")"])
if bracesCount["("]! < bracesCount[")"]!, buffer.last == ")" { buffer.removeLast() }
return
}
try _scanAbnormalStuff(into: &buffer)
}
_ = scanCharacters(from: .whitespaces)
try _scanAbnormalStuff(into: &buffer)
buffer = buffer.trimmingCharacters(in: .whitespaces)
_ = scanCharacters(from: .whitespaces)
}
private func scanDefaultValue(into parameter: inout FunctionParameter) throws {
var buffer = ""
try scanAbnormalStuff(into: &buffer)
parameter.defaultValue = buffer
}
}
extension FunctionParameterScanner {
private func scanCharacters(from characterSet: CharacterSet) -> String? {
var output = ""
while !isAtEnd, characterSet.isSuperset(of: .init(charactersIn: String(string[currentIndex]))) {
output.append(scanCharacter()!)
}
return output.isEmpty ? nil : output
}
private func scanString(_ substring: String) -> String? {
if let range = string[currentIndex...].range(of: substring), range.lowerBound == currentIndex {
return String(scan(to: range.upperBound))
} else {
return nil
}
}
private func scanCharacter() -> Character? {
guard !isAtEnd else { return nil }
currentIndex = string.index(after: currentIndex)
return string[string.index(before: currentIndex)]
}
private func scanToCharacter(_ character: Character) -> String? {
unscanned.firstIndex(of: character).map { String(scan(to: $0)) }
}
private func scanToCharacters(from characterSet: CharacterSet) -> String? {
unscanned
.firstIndex { characterSet.isSuperset(of: .init(charactersIn: String($0))) }
.map { String(scan(to: $0)) }
}
private func scan(to index: String.Index) -> Substring {
let result = unscanned[..<index]
currentIndex = index
return result
}
}
extension String {
func countOccurances(of characters: [Character]) -> [Character: Int] {
var buffer = characters.reduce(into: [Character: Int]()) { $0[$1] = 0 }
var floatingIndex = startIndex
while let index = self[floatingIndex...].firstIndex(where: characters.contains) {
floatingIndex = self.index(after: index)
buffer[self[index]]! += 1
}
return buffer
}
func countOccurances(of character: Character) -> Int {
return countOccurances(of: [character])[character]!
}
}
@maximkrouk
Copy link
Author

maximkrouk commented Apr 26, 2020

WIP

Usage:

extractParametersString does not support generic functions & closures yet

var parametersString = "(@FunctionBuilder parameterLabel name: () -> Type = andDefaultValue, or just: Type)"
try FunctionParameterScanner(paramtersString).scan()
[
    FunctionParameter(
        functionBuilder: "FunctionBuilder"
        label: "parameterLabel"
        name: "name"
        type: "() -> Type"
        defaultValue: "andDefaultValue"
    ),
    FunctionParameter(
        functionBuilder: nil
        label: "or"
        name: "just"
        type: "Type"
        defaultValue: "nil"
    )
]

Back to index

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment