Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IB connection rule #9

Closed
wants to merge 15 commits into from
31 changes: 29 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"object": {
"pins": [
{
"package": "Clang_C",
"repositoryURL": "https://github.com/norio-nomura/Clang_C.git",
"state": {
"branch": null,
"revision": "90a9574276f0fd17f02f58979423c3fd4d73b59e",
"version": "1.0.2"
}
},
{
"package": "Commandant",
"repositoryURL": "https://github.com/Carthage/Commandant.git",
Expand Down Expand Up @@ -46,13 +55,31 @@
"version": "4.2.5"
}
},
{
"package": "SourceKit",
"repositoryURL": "https://github.com/norio-nomura/SourceKit.git",
"state": {
"branch": null,
"revision": "18eaa67ca44443bbe39646916792b9f0c98dbaa1",
"version": "1.0.1"
}
},
{
"package": "SourceKitten",
"repositoryURL": "https://github.com/jpsim/SourceKitten.git",
"state": {
"branch": null,
"revision": "71e8297e5d95118588f8aa8e1de892762346dc9d",
"version": "0.18.4"
}
},
{
"package": "Yams",
"repositoryURL": "https://github.com/jpsim/Yams.git",
"state": {
"branch": null,
"revision": "95f45caf07472ec78223ebada45255086a85b01a",
"version": "0.5.0"
"revision": "90a76f3cdb552b8e6aea5dabdd6c3578b8a7ad45",
"version": "0.4.1"
}
}
]
Expand Down
8 changes: 5 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ let package = Package(
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/drmohundro/SWXMLHash.git", from: "4.0.0"),
.package(url: "https://github.com/Carthage/Commandant.git", .branch("master")),
.package(url: "https://github.com/jpsim/Yams.git", from: "0.4.1")
.package(url: "https://github.com/jpsim/Yams.git", from: "0.4.1"),
.package(url: "https://github.com/jpsim/SourceKitten.git", from: "0.18.4")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand All @@ -25,11 +26,12 @@ let package = Package(
dependencies: ["IBLinterKit"]),
.target(
name: "IBLinterCore",
dependencies: ["SWXMLHash"]),
dependencies: ["SWXMLHash", "SourceKittenFramework"]),
.target(
name: "IBLinterKit",
dependencies: ["IBLinterCore", "Commandant", "Yams"]),
.testTarget(name: "IBLinterKitTest",
dependencies: ["IBLinterKit"])
dependencies: ["IBLinterKit"],
exclude: ["Resources"])
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by SaitoYuta on 2017/12/13.
//

public protocol InterfaceBuilderFile {
public protocol FileProtocol {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to analyze not only xib or storyboard but also swift

var pathString: String { get }
var fileName: String { get }
}
22 changes: 22 additions & 0 deletions Sources/IBLinterCore/Identifiable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Identifiable.swift
// IBLinterCore
//
// Created by SaitoYuta on 2018/01/06.
//

public protocol Identifiable {
var id: String { get }
}

extension Identifiable where Self: ViewProtocol {

// TODO: use cache
public func find(by id: String) -> InterfaceBuilderNode.View? {
if let matched = subviews?.first(where: { $0.id == id }) {
return matched
} else {
return subviews?.lazy.flatMap { $0.find(by: id) }.first
}
}
}
3 changes: 3 additions & 0 deletions Sources/IBLinterCore/InterfaceBuilderNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ public extension InterfaceBuilderNode {
case unsupportedConstraint(String)
case unsupportedTableViewDataMode(String)
case unsupportedColorSpace(String)
case unsupportedConnectionType(String)

public var description: String {
switch self {
Expand All @@ -165,6 +166,8 @@ public extension InterfaceBuilderNode {
return "unsupported dataMode '\(name)'"
case .unsupportedColorSpace(let colorSpace):
return "unsupported color space '\(colorSpace)'"
case .unsupportedConnectionType(let connectionType):
return "unsupported connection type \(connectionType)"
}
}
}
Expand Down
87 changes: 86 additions & 1 deletion Sources/IBLinterCore/InterfaceBuilderView.swift

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions Sources/IBLinterCore/InterfaceBuilderViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public protocol ViewControllerProtocol {
var customClass: String? { get }
var customModule: String? { get }
var customModuleProvider: String? { get }
var connections: [InterfaceBuilderNode.View.Connection]? { get }
var layoutGuides: [InterfaceBuilderNode.ViewControllerLayoutGuide]? { get }
var rootView: ViewProtocol? { get }
}
Expand All @@ -31,6 +32,7 @@ extension InterfaceBuilderNode {
public var customClass: String? { return _viewController.customClass }
public var customModule: String? { return _viewController.customModule }
public var customModuleProvider: String? { return _viewController.customModuleProvider }
public var connections: [InterfaceBuilderNode.View.Connection]? { return _viewController.connections }
public var layoutGuides: [InterfaceBuilderNode.ViewControllerLayoutGuide]? {
return _viewController.layoutGuides
}
Expand Down Expand Up @@ -72,6 +74,7 @@ extension InterfaceBuilderNode {
public let customClass: String?
public let customModule: String?
public let customModuleProvider: String?
public let connections: [InterfaceBuilderNode.View.Connection]?
public let layoutGuides: [ViewControllerLayoutGuide]?
public let view: View.View?
public var rootView: ViewProtocol? { return view }
Expand All @@ -82,6 +85,7 @@ extension InterfaceBuilderNode {
customClass: xml.attributeValue(of: "customClass"),
customModule: xml.attributeValue(of: "customModule"),
customModuleProvider: xml.attributeValue(of: "customModuleProvider"),
connections: xml.byKey("connections")?.childrenNode.flatMap(decodeValue),
layoutGuides: xml.byKey("layoutGuides")?.byKey("viewControllerLayoutGuide")?.allElements.flatMap(decodeValue),
view: xml.byKey("view").flatMap(decodeValue)
)
Expand All @@ -93,6 +97,7 @@ extension InterfaceBuilderNode {
public let customClass: String?
public let customModule: String?
public let customModuleProvider: String?
public let connections: [InterfaceBuilderNode.View.Connection]?
public let layoutGuides: [ViewControllerLayoutGuide]?
public let tableView: View.TableView?
public var rootView: ViewProtocol? { return tableView }
Expand All @@ -103,6 +108,7 @@ extension InterfaceBuilderNode {
customClass: xml.attributeValue(of: "customClass"),
customModule: xml.attributeValue(of: "customModule"),
customModuleProvider: xml.attributeValue(of: "customModuleProvider"),
connections: xml.byKey("connections")?.childrenNode.flatMap(decodeValue),
layoutGuides: xml.byKey("layoutGuides")?.byKey("viewControllerLayoutGuide")?.allElements.flatMap(decodeValue),
tableView: xml.byKey("tableView").flatMap(decodeValue)
)
Expand Down
2 changes: 1 addition & 1 deletion Sources/IBLinterCore/StoryboardFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import SWXMLHash
import Foundation

public class StoryboardFile: InterfaceBuilderFile {
public class StoryboardFile: FileProtocol {
public var pathString: String
public var fileName: String {
return pathString.components(separatedBy: "/").last!
Expand Down
20 changes: 20 additions & 0 deletions Sources/IBLinterCore/SwiftFile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// SwiftFile.swift
// IBLinterCore
//
// Created by SaitoYuta on 2018/01/12.
//

import Foundation

public class SwiftFile: FileProtocol {
public var pathString: String
public var fileName: String {
return pathString.components(separatedBy: "/").last!
}

public init(path: String) {
self.pathString = path
}
}

163 changes: 163 additions & 0 deletions Sources/IBLinterCore/SwiftIBParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//
// SwiftIBParser.swift
// IBLinterCore
//
// Created by SaitoYuta on 2018/01/06.
//

import Foundation
import SourceKittenFramework

public class SwiftIBParser {

public struct Class {
public let file: SwiftFile
public let name: String
public let connections: [Connection]
public let inheritedClassNames: [String]
public let declaration: Declaration

public init(file: SwiftFile, name: String, connections: [Connection],
inheritedClassNames: [String], declaration: Declaration) {
self.file = file
self.name = name
self.connections = connections
self.declaration = declaration
self.inheritedClassNames = inheritedClassNames
}
}

public enum Connection {
case action(selector: String, declaration: Declaration)
case outlet(property: String, isOptional: Bool, declaration: Declaration)

var swiftFile: SwiftFile? {
switch self {
case .action(_, let declaration),
.outlet(_, _, let declaration):
return declaration.path.map { SwiftFile.init(path: $0) }
}
}
}

public struct Declaration {

public let line: Int
public let column: Int
public let path: String?

public init(line: Int, column: Int, path: String?) {
self.line = line
self.column = column
self.path = path
}

public init(file: File, offset: Int64) {
let fileOffset = type(of: self).getLineColumnNumber(of: file, offset: Int(offset))

self.line = fileOffset.line
self.column = fileOffset.column
// SourceKitten use no file scheme prefix path
self.path = file.path.map { URL.init(fileURLWithPath: $0).absoluteString }
}

private static func getLineColumnNumber(of file: File, offset: Int) -> (line: Int, column: Int) {
let range = file.contents.startIndex..<file.contents.index(file.contents.startIndex, offsetBy: offset)
let subString = file.contents[range]
let lines = subString.components(separatedBy: "\n")

if let column = lines.last?.characters.count {
return (line: lines.count, column: column)
}
return (line: lines.count, column: 0)
}
}

public private(set) var classNameToStructure: [String: Class] = [:]

public init(swiftFilePaths: [String]) {

swiftFilePaths.forEach(mappingFile)
}

private func mappingFile(at path: String) {
guard let file = File(path: path) else { return }
let fileStructure = Structure(file: file)

fileStructure.dictionary.substructure.forEach { [weak self] structure in
var connections: [Connection] = []

guard let kind = structure["key.kind"] as? String, let name = structure["key.name"] as? String,
let nameOffset64 = structure["key.nameoffset"] as? Int64,
let inheritedTypes = structure["key.inheritedtypes"] as? [[String: String]],
kind == "source.lang.swift.decl.class" || kind == "source.lang.swift.decl.extension" else { return }

structure.substructure.forEach { insideStructure in
guard let attributes = insideStructure["key.attributes"] as? [[String: String]],
let propertyName = insideStructure["key.name"] as? String else { return }

let isOutlet = attributes.contains { $0.values.contains("source.decl.attribute.iboutlet") }
if isOutlet, let nameOffset64 = insideStructure["key.nameoffset"] as? Int64 {
connections.append(.outlet(property: propertyName, isOptional: insideStructure.isOptional,
declaration: .init(file: file, offset: nameOffset64)))
}

let isIBAction = attributes.contains { $0.values.contains("source.decl.attribute.ibaction") }

if isIBAction, let selectorName = insideStructure["key.selector_name"] as? String,
let nameOffset64 = insideStructure["key.nameoffset"] as? Int64 {
connections.append(.action(selector: selectorName,
declaration: .init(file: file, offset: nameOffset64)))
}
}

self?.classNameToStructure[name] = Class(file: SwiftFile(path: path),
name: name, connections: connections,
inheritedClassNames: inheritedTypes.flatMap { $0["key.name"] },
declaration: .init(file: file, offset: nameOffset64))
}
}
}

private extension Dictionary where Key: ExpressibleByStringLiteral {
var substructure: [[String: SourceKitRepresentable]] {
let substructure = self["key.substructure"] as? [SourceKitRepresentable] ?? []
return substructure.flatMap { $0 as? [String: SourceKitRepresentable] }
}

var isOptional: Bool {
if let typename = self["key.typename"] as? String,
let optionalString = typename.characters.last {
return optionalString == "?"
}
return false
}
}

extension SwiftIBParser.Class: Equatable {
public static func ==(lhs: SwiftIBParser.Class, rhs: SwiftIBParser.Class) -> Bool {
return lhs.name == rhs.name && lhs.connections == rhs.connections
}
}

extension SwiftIBParser.Connection: Equatable {
public static func ==(lhs: SwiftIBParser.Connection, rhs: SwiftIBParser.Connection) -> Bool {
switch (lhs, rhs) {
case (.action(let selector1, let declaration1),
.action(let selector2, let declaration2)):
return selector1 == selector2 && declaration1 == declaration2
case (.outlet(let property1, let isOptional1, let declaration1),
.outlet(let property2, let isOptional2, let declaration2)):
return property1 == property2 && isOptional1 == isOptional2 && declaration1 == declaration2
default: return false
}
}
}

extension SwiftIBParser.Declaration: Equatable {
public static func ==(lhs: SwiftIBParser.Declaration, rhs: SwiftIBParser.Declaration) -> Bool {
return lhs.column == rhs.column &&
lhs.line == rhs.line &&
lhs.path == rhs.path
}
}
2 changes: 1 addition & 1 deletion Sources/IBLinterCore/XibFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import SWXMLHash
import Foundation

public class XibFile: InterfaceBuilderFile {
public class XibFile: FileProtocol {
public var pathString: String
public var fileName: String {
return pathString.components(separatedBy: "/").last!
Expand Down
Loading