Last active
May 30, 2020 14:21
-
-
Save maximkrouk/fcd6d2f8b9f633c1062ff90ba2e90338 to your computer and use it in GitHub Desktop.
Swift function parameter scanner
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: ¶meter) | |
try scanTypeAndDefaultValue(into: ¶meter) | |
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: ¶meter) | |
} 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: ¶meter) | |
} | |
} | |
} 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]! | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
WIP
Usage:
Back to index