Lint rules of hooks (#92)
* Remove broken test * Add OneRenderFunctionRule * Init RenderCorespondToNonPureComponentProtocolRule * Fix OneRenderFunctionRule * Apply swiftformat * Fix RenderCorespondToNonPureComponentProtocolRule * Add render check to RenderCorespondToNonPureComponentProtocolRule * Update TokenVisitor * Update getNodes * Add TokenLint helpers Add GetRender, IsConformance, TokenTypes * Remove RenderCorespondToNonPureComponentProtocolRule * Format comments * Fix GetRender * Remove TokenTypes * Replace strings with SyntaxKind rawValue * Add function to walk in graph * Add TokenVisitor init function * Add GraphWalkers to project * Update isConformance function * Update OneRenderFunctionRule * Update getFirstChildOf function * Update GraphWalkers * Add getRender function * Add RenderGetsHooksRule * Update RenderGetsHooksRule * Update RenderGetsHooksRule * Apply swiftformat * Rename getNodes to children * Remove render from GetRender * Remove IsConformance * Mode nodes helpers to `Node.swift` * Add test to Node * Fix Unused Optional Binding Violation * Fix Leading Whitespace Violation * Apply swiftformat * Fix typos * Fix typos * Remove unused code * Clean code * Move isInherited to Node * Refactor hasTokamakImport * Refactor Node * Add helpers to TokamakCLITests * Refactor code * Update TokamakCLI main * Refactor GetRender * Fix typo * Refactor rules * Refactor TokenVIsitor * Add TokamakCLI to .travis.yml * Move NodeStruct to ValidationTests * Add TEST_PATH to TokamakCLI * Refactor StyleViolation * Refactor Rule * Add SwiftCLI to Package * Apply swiftformat * Regenerate project * Update project.pbxproj * Remove package for swift-4.2
This commit is contained in:
parent
78fa74d592
commit
4f02218fdb
56
.travis.yml
56
.travis.yml
|
@ -7,37 +7,31 @@ language: swift
|
|||
|
||||
jobs:
|
||||
include:
|
||||
- stage: lint
|
||||
osx_image: xcode10.2
|
||||
language: swift
|
||||
before_install:
|
||||
- brew install swiftformat
|
||||
script:
|
||||
# temporarily disabled due to Swift ABI issues on Xcode 10.2, Mojave 10.14.4
|
||||
- swiftformat --lint --verbose .
|
||||
- swiftlint
|
||||
- &test
|
||||
stage: test
|
||||
osx_image: xcode10
|
||||
language: swift
|
||||
install: skip
|
||||
env: TEST_DEVICE='platform=iOS Simulator,OS=12.0,name=iPhone SE'
|
||||
script:
|
||||
- xcodebuild -scheme TokamakUIKit -sdk iphonesimulator | xcpretty
|
||||
- xcodebuild -scheme TokamakAppKit -sdk macosx10.14 | xcpretty
|
||||
# this runs the build the second time, but it's the only way to make sure
|
||||
# that `Package.swift` is in a good state
|
||||
- swift build --target Tokamak
|
||||
- xcodebuild test -enableCodeCoverage YES -scheme Tokamak | xcpretty
|
||||
- <<: *test
|
||||
osx_image: xcode10.1
|
||||
env: TEST_DEVICE='platform=iOS Simulator,OS=12.1,name=iPhone SE'
|
||||
- <<: *test
|
||||
osx_image: xcode10.2
|
||||
env: TEST_DEVICE='platform=iOS Simulator,OS=12.2,name=iPhone SE'
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
- stage: lint
|
||||
osx_image: xcode10.2
|
||||
language: swift
|
||||
before_install:
|
||||
- brew install swiftformat
|
||||
script:
|
||||
# temporarily disabled due to Swift ABI issues on Xcode 10.2, Mojave 10.14.4
|
||||
- swiftformat --lint --verbose .
|
||||
- swiftlint
|
||||
- &test
|
||||
stage: test
|
||||
osx_image: xcode10.2
|
||||
language: swift
|
||||
install: skip
|
||||
env: TEST_DEVICE='platform=iOS Simulator,OS=12.2,name=iPhone SE'
|
||||
script:
|
||||
- xcodebuild -scheme TokamakUIKit -sdk iphonesimulator | xcpretty
|
||||
- xcodebuild -scheme TokamakAppKit -sdk macosx | xcpretty
|
||||
- xcodebuild test -scheme TokamakCLI -sdk macosx | xcpretty
|
||||
# this runs the build the second time, but it's the only way to make sure
|
||||
# that `Package.swift` is in a good state
|
||||
- swift build --target Tokamak
|
||||
- xcodebuild test -enableCodeCoverage YES -scheme Tokamak | xcpretty
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
# before_install:
|
||||
# - gem install cocoapods --pre # Since Travis is not always on latest version
|
||||
# - brew outdated carthage || brew upgrade carthage
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
// swift-tools-version:4.2
|
||||
// The swift-tools-version declares the minimum version of Swift required to
|
||||
// build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Tokamak",
|
||||
products: [
|
||||
// Products define the executables and libraries produced by a package,
|
||||
// and make them visible to other packages.
|
||||
.library(
|
||||
name: "Tokamak",
|
||||
targets: ["Tokamak"]
|
||||
),
|
||||
.library(
|
||||
name: "TokamakUIKit",
|
||||
targets: ["TokamakUIKit"]
|
||||
),
|
||||
.library(
|
||||
name: "TokamakAppKit",
|
||||
targets: ["TokamakAppKit"]
|
||||
),
|
||||
.library(
|
||||
name: "TokamakTestRenderer",
|
||||
targets: ["TokamakTestRenderer"]
|
||||
),
|
||||
.library(
|
||||
name: "TokamakLint",
|
||||
targets: ["TokamakLint"]
|
||||
),
|
||||
.executable(name: "tokamak", targets: ["TokamakCLI"]),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-syntax.git", .exact("0.50000.0")),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define
|
||||
// a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products
|
||||
// in packages which this package depends on.
|
||||
.target(
|
||||
name: "Tokamak",
|
||||
dependencies: []
|
||||
),
|
||||
.target(
|
||||
name: "TokamakDemo",
|
||||
dependencies: ["Tokamak"]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakUIKit",
|
||||
dependencies: ["Tokamak"]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakAppKit",
|
||||
dependencies: ["Tokamak"]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakTestRenderer",
|
||||
dependencies: ["Tokamak"]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakCLI",
|
||||
dependencies: ["TokamakLint"]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakLint",
|
||||
dependencies: ["SwiftSyntax"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "TokamakTests",
|
||||
dependencies: ["TokamakTestRenderer"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "TokamakCLITests",
|
||||
dependencies: ["TokamakLint"]
|
||||
),
|
||||
]
|
||||
)
|
|
@ -36,4 +36,4 @@ class LintCommand: Command {
|
|||
let TokamakCLI = CLI(name: "TokamakCLI", version: "0.1.2", description: "Tokamak CLI tools")
|
||||
|
||||
TokamakCLI.commands = [LintCommand()]
|
||||
TokamakCLI.go()
|
||||
TokamakCLI.goAndExit()
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// GetRender.swift
|
||||
// TokamakLint
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 5/8/19.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftSyntax
|
||||
|
||||
extension Array: Error where Element == StyleViolation {}
|
||||
|
||||
func getRender(from node: Node, at file: String) throws -> Node {
|
||||
let renders = node.children(with: "render").filter {
|
||||
// check if render type is function
|
||||
var memberDeclListItem = $0
|
||||
while memberDeclListItem.text != SyntaxKind.memberDeclListItem.rawValue
|
||||
&& memberDeclListItem.parent != nil {
|
||||
guard let parent = memberDeclListItem.parent else { break }
|
||||
memberDeclListItem = parent
|
||||
}
|
||||
guard let functionDecl = memberDeclListItem.children.first,
|
||||
functionDecl.text == SyntaxKind.functionDecl.rawValue else { return false }
|
||||
|
||||
// check if render is static
|
||||
let staticModifier = memberDeclListItem.children(
|
||||
with: SyntaxKind.declModifier.rawValue
|
||||
).filter {
|
||||
guard let child = $0.children.first else { return false }
|
||||
return child.text == "static"
|
||||
}
|
||||
guard staticModifier.first != nil else { return false }
|
||||
|
||||
// check if render is on first layer of component
|
||||
return functionDecl.children.map {
|
||||
$0.text
|
||||
}.contains("render")
|
||||
}
|
||||
|
||||
guard renders.count == 1 else {
|
||||
if renders.count > 1 {
|
||||
throw renders.map {
|
||||
StyleViolation(
|
||||
ruleDescription: OneRenderFunctionRule.description,
|
||||
location: Location(
|
||||
file: file,
|
||||
line: $0.range.startRow,
|
||||
character: $0.range.startColumn
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw [StyleViolation(
|
||||
ruleDescription: OneRenderFunctionRule.description,
|
||||
location: Location(
|
||||
file: file,
|
||||
line: node.range.startRow,
|
||||
character: node.range.startColumn
|
||||
)
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
return renders[0]
|
||||
}
|
|
@ -14,7 +14,7 @@ public struct Location: CustomStringConvertible, Comparable {
|
|||
public let character: Int
|
||||
public var description: String {
|
||||
// Xcode likes warnings and errors in the following format:
|
||||
// {full_path_to_file}{:line}{:character}: {error,warning}: {content}
|
||||
// {full_path_to_file}{:line}{:character}: {error, warning}: {content}
|
||||
return "\(file):\(line):\(character)"
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
//
|
||||
// Node.swift
|
||||
// TokamakLint
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 5/11/19.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftSyntax
|
||||
|
||||
public class Node {
|
||||
var text: String
|
||||
private(set) var children = [Node]()
|
||||
private(set) weak var parent: Node?
|
||||
var range = Range(startRow: 0, startColumn: 0, endRow: 0, endColumn: 0)
|
||||
|
||||
struct Range {
|
||||
var startRow: Int
|
||||
var startColumn: Int
|
||||
var endRow: Int
|
||||
var endColumn: Int
|
||||
}
|
||||
|
||||
init(text: String) {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
func add(node: Node) {
|
||||
node.parent = self
|
||||
children.append(node)
|
||||
}
|
||||
|
||||
func children(with type: String) -> [Node] {
|
||||
guard children.first != nil else { return [] }
|
||||
var nodes: [Node] = []
|
||||
walkAndGrab(get: type, from: self, to: &nodes)
|
||||
return nodes
|
||||
}
|
||||
|
||||
func walkAndGrab(
|
||||
get type: String,
|
||||
from node: Node,
|
||||
to list: inout [Node]
|
||||
) {
|
||||
if node.text == type {
|
||||
list.append(node)
|
||||
}
|
||||
for child in node.children {
|
||||
walkAndGrab(get: type, from: child, to: &list)
|
||||
}
|
||||
}
|
||||
|
||||
func firstParent(of type: String) -> Node? {
|
||||
var nodeParent = self
|
||||
|
||||
while nodeParent.text != type && nodeParent.parent != nil {
|
||||
guard let parent = nodeParent.parent else { break }
|
||||
nodeParent = parent
|
||||
}
|
||||
|
||||
guard nodeParent.text == type else { return nil }
|
||||
|
||||
return nodeParent
|
||||
}
|
||||
|
||||
func firstChild(of type: String) -> Node? {
|
||||
guard !children.isEmpty else { return nil }
|
||||
|
||||
return children.first { $0.text == type } ??
|
||||
children.compactMap { $0.firstChild(of: type) }.first
|
||||
}
|
||||
|
||||
func isInherited(from type: String) -> Bool {
|
||||
let typeNodes = children(
|
||||
with: SyntaxKind.simpleTypeIdentifier.rawValue
|
||||
)
|
||||
return typeNodes.flatMap { $0.children }.contains { $0.text == type }
|
||||
}
|
||||
}
|
|
@ -56,8 +56,7 @@ func checkFile(_ path: String) throws -> [StyleViolation] {
|
|||
private func walkParsedTree(_ path: String) throws -> TokenVisitor {
|
||||
let fileURL = URL(fileURLWithPath: path)
|
||||
let parsedTree = try SyntaxTreeParser.parse(fileURL)
|
||||
let visitor = TokenVisitor()
|
||||
visitor.path = path
|
||||
let visitor = TokenVisitor(path: path)
|
||||
parsedTree.walk(visitor)
|
||||
return visitor
|
||||
}
|
||||
|
@ -67,14 +66,8 @@ private func isSwiftFile(_ path: String) -> Bool {
|
|||
}
|
||||
|
||||
private func hasTokamakImport(from visitor: TokenVisitor) -> Bool {
|
||||
var doesTokamakImportExist = false
|
||||
let imports = visitor.getNodes(get: "ImportDecl", from: visitor.tree[0])
|
||||
for importNode in imports {
|
||||
let importModules = visitor.getNodes(get: "AccessPathComponent", from: importNode)
|
||||
for module in importModules where module.children[0].text == "Tokamak" {
|
||||
doesTokamakImportExist = true
|
||||
}
|
||||
}
|
||||
|
||||
return doesTokamakImportExist
|
||||
return visitor.root.children(with: SyntaxKind.importDecl.rawValue)
|
||||
.flatMap { $0.children(with: SyntaxKind.accessPathComponent.rawValue) }
|
||||
.compactMap { $0.children.first }
|
||||
.contains { $0.text == "Tokamak" }
|
||||
}
|
||||
|
|
|
@ -9,14 +9,19 @@ import Foundation
|
|||
import SwiftSyntax
|
||||
|
||||
class TokenVisitor: SyntaxVisitor {
|
||||
public var tree = [Node]()
|
||||
public var path: String?
|
||||
public var current: Node?
|
||||
// Syntax tree is always has one 'SourceFile' node as a child
|
||||
var root = Node(text: "Root")
|
||||
var path: String
|
||||
private var current: Node?
|
||||
|
||||
init(path: String) {
|
||||
self.path = path
|
||||
}
|
||||
|
||||
var row = 0
|
||||
var column = 0
|
||||
|
||||
public override func visitPre(_ node: Syntax) {
|
||||
override func visitPre(_ node: Syntax) {
|
||||
var syntax = "\(type(of: node))"
|
||||
|
||||
if syntax.hasSuffix("Syntax") {
|
||||
|
@ -28,12 +33,12 @@ class TokenVisitor: SyntaxVisitor {
|
|||
if let current = current {
|
||||
current.add(node: syntaxNode)
|
||||
} else {
|
||||
tree.append(syntaxNode)
|
||||
root.add(node: syntaxNode)
|
||||
}
|
||||
current = syntaxNode
|
||||
}
|
||||
|
||||
public override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind {
|
||||
override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind {
|
||||
guard let current = current else { return .visitChildren }
|
||||
|
||||
current.text = token.text
|
||||
|
@ -65,7 +70,7 @@ class TokenVisitor: SyntaxVisitor {
|
|||
return .visitChildren
|
||||
}
|
||||
|
||||
public override func visitPost(_ node: Syntax) {
|
||||
override func visitPost(_ node: Syntax) {
|
||||
// go up after full node walk
|
||||
current = current?.parent
|
||||
}
|
||||
|
@ -105,64 +110,4 @@ class TokenVisitor: SyntaxVisitor {
|
|||
row += comments.count - 1
|
||||
column += last.count
|
||||
}
|
||||
|
||||
public func getNodes(get type: String, from node: Node) -> [Node] {
|
||||
var nodes: [Node] = []
|
||||
walkAndGrab(get: type, from: node, to: &nodes)
|
||||
return nodes
|
||||
}
|
||||
|
||||
private func walkAndGrab(
|
||||
get type: String,
|
||||
from node: Node,
|
||||
to list: inout [Node]
|
||||
) {
|
||||
if node.text == type {
|
||||
list.append(node)
|
||||
}
|
||||
for child in node.children {
|
||||
walkAndGrab(get: type, from: child, to: &list)
|
||||
}
|
||||
}
|
||||
|
||||
func isInherited(node: Node, from type: String) -> Bool {
|
||||
var str: String = ""
|
||||
let typeNodes = getNodes(get: "SimpleTypeIdentifier", from: node)
|
||||
for node in typeNodes {
|
||||
for type in node.children {
|
||||
str.append("\(type.text)")
|
||||
}
|
||||
}
|
||||
return str.contains(type)
|
||||
}
|
||||
}
|
||||
|
||||
public class Node {
|
||||
var text: String
|
||||
var children = [Node]()
|
||||
weak var parent: Node?
|
||||
var range = Range(startRow: 0, startColumn: 0, endRow: 0, endColumn: 0)
|
||||
|
||||
struct Range: Encodable {
|
||||
var startRow: Int
|
||||
var startColumn: Int
|
||||
var endRow: Int
|
||||
var endColumn: Int
|
||||
}
|
||||
|
||||
enum CodingKeys: CodingKey {
|
||||
case text
|
||||
case children
|
||||
case range
|
||||
case token
|
||||
}
|
||||
|
||||
init(text: String) {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
func add(node: Node) {
|
||||
node.parent = self
|
||||
children.append(node)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,11 +14,10 @@ protocol Rule {
|
|||
}
|
||||
|
||||
extension Rule {
|
||||
public static func validate(path: String) throws -> [StyleViolation] {
|
||||
static func validate(path: String) throws -> [StyleViolation] {
|
||||
let fileURL = URL(fileURLWithPath: path)
|
||||
let parsedTree = try SyntaxTreeParser.parse(fileURL)
|
||||
let visitor = TokenVisitor()
|
||||
visitor.path = path
|
||||
let visitor = TokenVisitor(path: path)
|
||||
parsedTree.walk(visitor)
|
||||
return validate(visitor: visitor)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// OneRenderFunctionRule.swift
|
||||
// TokamakCLI
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 5/6/19.
|
||||
//
|
||||
|
||||
import SwiftSyntax
|
||||
|
||||
struct OneRenderFunctionRule: Rule {
|
||||
static let description = RuleDescription(
|
||||
type: OneRenderFunctionRule.self,
|
||||
name: "One Render Function",
|
||||
description: "The component should have only one render function"
|
||||
)
|
||||
|
||||
static func validate(visitor: TokenVisitor) -> [StyleViolation] {
|
||||
do {
|
||||
_ = try getRender(from: visitor.root, at: visitor.path)
|
||||
} catch let error as [StyleViolation] {
|
||||
return error
|
||||
} catch {
|
||||
print(error)
|
||||
return []
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
|
@ -5,19 +5,18 @@
|
|||
// Created by Matvii Hodovaniuk on 4/9/19.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftSyntax
|
||||
|
||||
struct PropsIsEquatableRule: Rule {
|
||||
public static let description = RuleDescription(
|
||||
static let description = RuleDescription(
|
||||
type: PropsIsEquatableRule.self,
|
||||
name: "Props is Equatable",
|
||||
description: "Component Props type shoud conformance to Equatable protocol"
|
||||
description: "Component Props type should conform to Equatable protocol"
|
||||
)
|
||||
|
||||
public static func validate(visitor: TokenVisitor) -> [StyleViolation] {
|
||||
static func validate(visitor: TokenVisitor) -> [StyleViolation] {
|
||||
var violations: [StyleViolation] = []
|
||||
let structs = visitor.getNodes(get: "StructDecl", from: visitor.tree[0])
|
||||
let structs = visitor.root.children(with: SyntaxKind.structDecl.rawValue)
|
||||
for structNode in structs {
|
||||
// sometimes there are additional children `ModifierList`
|
||||
// it will be better to filter array to find out if struct name is
|
||||
|
@ -28,15 +27,14 @@ struct PropsIsEquatableRule: Rule {
|
|||
guard propsNodes.count != 0 else { continue }
|
||||
let propsNode = propsNodes[0]
|
||||
|
||||
guard let propsParent = propsNode.parent, !visitor.isInherited(
|
||||
node: propsParent,
|
||||
guard let propsParent = propsNode.parent, !propsParent.isInherited(
|
||||
from: "Equatable"
|
||||
) else { continue }
|
||||
violations.append(
|
||||
StyleViolation(
|
||||
ruleDescription: description,
|
||||
location: Location(
|
||||
file: visitor.path ?? "",
|
||||
file: visitor.path,
|
||||
line: propsNode.range.startRow,
|
||||
character: propsNode.range.startColumn
|
||||
)
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
//
|
||||
// RenderGetsHooksRule.swift
|
||||
// TokamakLint
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 5/6/19.
|
||||
//
|
||||
|
||||
import SwiftSyntax
|
||||
|
||||
struct RenderGetsHooksRule: Rule {
|
||||
static let description = RuleDescription(
|
||||
type: RenderGetsHooksRule.self,
|
||||
name: "Render gets Hooks",
|
||||
description: "Non-pure render should get Hooks type argument"
|
||||
)
|
||||
|
||||
public static func validate(visitor: TokenVisitor) -> [StyleViolation] {
|
||||
do {
|
||||
let renderFunction = try getRender(from: visitor.root, at: visitor.path)
|
||||
guard let codeBlock = renderFunction.firstParent(
|
||||
of: SyntaxKind.codeBlockItem.rawValue
|
||||
) else { return [StyleViolation(
|
||||
ruleDescription: OneRenderFunctionRule.description,
|
||||
location: Location(
|
||||
file: visitor.path,
|
||||
line: renderFunction.range.startRow,
|
||||
character: renderFunction.range.startColumn
|
||||
)
|
||||
)] }
|
||||
guard let functionSignature = codeBlock.firstChild(
|
||||
of: SyntaxKind.functionSignature.rawValue
|
||||
) else { return [] }
|
||||
|
||||
let hooksArgument = functionSignature.children(
|
||||
with: SyntaxKind.simpleTypeIdentifier.rawValue
|
||||
).filter {
|
||||
guard let children = $0.children.first else { return false }
|
||||
return children.text == "Hooks"
|
||||
}
|
||||
|
||||
guard !hooksArgument.isEmpty else {
|
||||
return [StyleViolation(
|
||||
ruleDescription: OneRenderFunctionRule.description,
|
||||
location: Location(
|
||||
file: visitor.path,
|
||||
line: renderFunction.range.startRow,
|
||||
character: renderFunction.range.startColumn
|
||||
)
|
||||
)]
|
||||
}
|
||||
} catch let error as [StyleViolation] {
|
||||
return error
|
||||
} catch {
|
||||
print(error)
|
||||
return []
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// Helpers.swift
|
||||
// TokamakCLITests
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 5/13/19.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
struct UnexpectedNilError: Error {}
|
||||
|
||||
func srcRoot() throws -> String {
|
||||
guard let src = ProcessInfo.processInfo.environment["TEST_PATH"] else {
|
||||
throw UnexpectedNilError()
|
||||
}
|
||||
return src
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
//
|
||||
// NodeTests.swift
|
||||
// TokamakLint
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 5/11/19.
|
||||
//
|
||||
|
||||
import SwiftSyntax
|
||||
@testable import TokamakLint
|
||||
import XCTest
|
||||
|
||||
final class NodeTests: XCTestCase {
|
||||
func testCollectChildren() throws {
|
||||
let houseLannister = Node(text: "Tywin")
|
||||
|
||||
houseLannister.add(node: Node(text: "Jaime"))
|
||||
houseLannister.add(node: Node(text: "Cersei"))
|
||||
houseLannister.add(node: Node(text: "Tyrion"))
|
||||
|
||||
XCTAssertEqual(houseLannister.children(with: "Jaime").count, 1)
|
||||
XCTAssertEqual(houseLannister.children(with: "JonSnow").count, 0)
|
||||
|
||||
let cerseiChildren = Node(text: "Cersei")
|
||||
|
||||
let joffrey = Node(text: "Joffrey")
|
||||
joffrey.add(node: Node(text: "Father: Jaime"))
|
||||
cerseiChildren.add(node: joffrey)
|
||||
|
||||
let myrcella = Node(text: "Myrcella")
|
||||
myrcella.add(node: Node(text: "Father: Jaime"))
|
||||
cerseiChildren.add(node: myrcella)
|
||||
|
||||
let tommen = Node(text: "Tommen")
|
||||
tommen.add(node: Node(text: "Father: Jaime"))
|
||||
cerseiChildren.add(node: tommen)
|
||||
|
||||
XCTAssertEqual(cerseiChildren.children(with: "Father: Jaime").count, 3)
|
||||
XCTAssertEqual(cerseiChildren.children(with: "Father: Robert").count, 0)
|
||||
}
|
||||
|
||||
func testFirstParent() throws {
|
||||
let houseTargaryen = Node(text: "House Targryen")
|
||||
|
||||
let aerys = Node(text: "Aerys \"the Mad\"")
|
||||
houseTargaryen.add(node: aerys)
|
||||
|
||||
let rhaegar = Node(text: "Rhaegar")
|
||||
aerys.add(node: rhaegar)
|
||||
|
||||
let jonSnow = Node(text: "Jon Snow")
|
||||
rhaegar.add(node: jonSnow)
|
||||
|
||||
guard let madKing = jonSnow.firstParent(of: "Aerys \"the Mad\"") else {
|
||||
throw UnexpectedNilError()
|
||||
}
|
||||
XCTAssertEqual(madKing.text, "Aerys \"the Mad\"")
|
||||
}
|
||||
|
||||
func testFirstChild() throws {
|
||||
let path = "\(try srcRoot())/NodeStruct.swift"
|
||||
let fileURL = URL(fileURLWithPath: path)
|
||||
let parsedTree = try SyntaxTreeParser.parse(fileURL)
|
||||
let visitor = TokenVisitor(path: path)
|
||||
parsedTree.walk(visitor)
|
||||
|
||||
guard let firstStruct = visitor.root.firstChild(
|
||||
of: SyntaxKind.structDecl.rawValue
|
||||
)
|
||||
else { throw UnexpectedNilError() }
|
||||
|
||||
XCTAssertEqual(firstStruct.children[1].text, "Img")
|
||||
}
|
||||
}
|
|
@ -8,24 +8,28 @@
|
|||
@testable import TokamakLint
|
||||
import XCTest
|
||||
|
||||
let srcRoot = ProcessInfo.processInfo.environment["SRCROOT"]!
|
||||
|
||||
final class TokamakLintTests: XCTestCase {
|
||||
func testFile() throws {
|
||||
let path = "\(srcRoot)/Sources/Tokamak/Components/Host/Button.swift"
|
||||
let result = try lintFile(path)
|
||||
XCTAssertEqual(result, [])
|
||||
}
|
||||
|
||||
func testPositivePropsIsEquatableRule() throws {
|
||||
let path = "\(srcRoot)/ValidationTests/TestPropsEquatable.swift"
|
||||
let path = "\(try srcRoot())/TestPropsEquatable.swift"
|
||||
let result = try PropsIsEquatableRule.validate(path: path)
|
||||
XCTAssertEqual(result, [])
|
||||
}
|
||||
|
||||
func testNegativePropsIsEquatableRule() throws {
|
||||
let path = "\(srcRoot)/ValidationTests/TestPropsIsNotEquatable.swift"
|
||||
let path = "\(try srcRoot())/TestPropsIsNotEquatable.swift"
|
||||
let result = try PropsIsEquatableRule.validate(path: path)
|
||||
XCTAssertEqual(result.count, 1)
|
||||
}
|
||||
|
||||
func testOneRenderFunctionRule() throws {
|
||||
let path = "\(try srcRoot())/PositiveTestHooksRule.swift"
|
||||
let result = try OneRenderFunctionRule.validate(path: path)
|
||||
XCTAssertEqual(result, [])
|
||||
}
|
||||
|
||||
func testRenderGetsHooksRule() throws {
|
||||
let path = "\(try srcRoot())/PositiveTestHooksRule.swift"
|
||||
let result = try RenderGetsHooksRule.validate(path: path)
|
||||
XCTAssertEqual(result, [])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,21 +11,19 @@ import XCTest
|
|||
|
||||
final class TokenVisitorTests: XCTestCase {
|
||||
func testRange() throws {
|
||||
let srcRoot = ProcessInfo.processInfo.environment["SRCROOT"]!
|
||||
let path = "\(srcRoot)/ValidationTests/TestPropsEquatable.swift"
|
||||
let path = "\(try srcRoot())/TestPropsEquatable.swift"
|
||||
|
||||
let fileURL = URL(fileURLWithPath: path)
|
||||
let parsedTree = try SyntaxTreeParser.parse(fileURL)
|
||||
let visitor = TokenVisitor()
|
||||
let visitor = TokenVisitor(path: path)
|
||||
|
||||
visitor.path = path
|
||||
parsedTree.walk(visitor)
|
||||
|
||||
let startRow = 11
|
||||
let endRow = 11
|
||||
let startColumn = 0
|
||||
let endColumn = 7
|
||||
let structs = visitor.getNodes(get: "StructDecl", from: visitor.tree[0])
|
||||
let structs = visitor.root.children(with: "StructDecl")
|
||||
let structDecl = structs[0].children[0]
|
||||
|
||||
XCTAssertEqual(startRow, structDecl.range.startRow)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -14,9 +14,23 @@
|
|||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Tokamak::TokamakDemo"
|
||||
BuildableName = "TokamakDemo.framework"
|
||||
BlueprintName = "TokamakDemo"
|
||||
BlueprintIdentifier = "Tokamak::TokamakUIKit"
|
||||
BuildableName = "TokamakUIKit.framework"
|
||||
BlueprintName = "TokamakUIKit"
|
||||
ReferencedContainer = "container:Tokamak.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Tokamak::Tokamak"
|
||||
BuildableName = "Tokamak.framework"
|
||||
BlueprintName = "Tokamak"
|
||||
ReferencedContainer = "container:Tokamak.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
|
@ -62,20 +76,6 @@
|
|||
ReferencedContainer = "container:Tokamak.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Tokamak::TokamakUIKit"
|
||||
BuildableName = "TokamakUIKit.framework"
|
||||
BlueprintName = "TokamakUIKit"
|
||||
ReferencedContainer = "container:Tokamak.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
|
@ -98,9 +98,9 @@
|
|||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Tokamak::Tokamak"
|
||||
BuildableName = "Tokamak.framework"
|
||||
BlueprintName = "Tokamak"
|
||||
BlueprintIdentifier = "Tokamak::TokamakDemo"
|
||||
BuildableName = "TokamakDemo.framework"
|
||||
BlueprintName = "TokamakDemo"
|
||||
ReferencedContainer = "container:Tokamak.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
|
@ -149,9 +149,9 @@
|
|||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Tokamak::TokamakDemo"
|
||||
BuildableName = "TokamakDemo.framework"
|
||||
BlueprintName = "TokamakDemo"
|
||||
BlueprintIdentifier = "Tokamak::TokamakUIKit"
|
||||
BuildableName = "TokamakUIKit.framework"
|
||||
BlueprintName = "TokamakUIKit"
|
||||
ReferencedContainer = "container:Tokamak.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
|
|
|
@ -64,8 +64,8 @@
|
|||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "SRCROOT"
|
||||
value = "${SRCROOT}"
|
||||
key = "TEST_PATH"
|
||||
value = "${SRCROOT}/ValidationTests"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// NodeStruct.swift
|
||||
// SwiftCLI
|
||||
//
|
||||
// This file help to test Node
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 5/12/19.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Img {
|
||||
let className = "Icon"
|
||||
let id = "uniq-id-0001"
|
||||
let height = 400
|
||||
let width = 300
|
||||
}
|
||||
|
||||
struct Div {
|
||||
let className = "Avatar"
|
||||
let id = "uniq-id-0000"
|
||||
let disabled = false
|
||||
|
||||
let content = Img()
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
//
|
||||
// TextField.swift
|
||||
// TokamakDemo
|
||||
//
|
||||
// Created by Matvii Hodovaniuk on 2/25/19.
|
||||
// Copyright © 2019 Tokamak. Tokamak is available under the Apache 2.0
|
||||
// license. See the LICENSE file for more info.
|
||||
//
|
||||
|
||||
import Tokamak
|
||||
|
||||
struct TextFieldExample: LeafComponent {
|
||||
typealias Props = Null
|
||||
|
||||
static func render(props: Props, hooks: Hooks) -> AnyNode {
|
||||
let text = hooks.state("")
|
||||
let textFieldStyle = Style(
|
||||
[
|
||||
Height.equal(to: 44),
|
||||
Width.equal(to: .parent),
|
||||
]
|
||||
)
|
||||
|
||||
return StackView.node(.init(
|
||||
[
|
||||
Leading.equal(to: .safeArea),
|
||||
Trailing.equal(to: .safeArea),
|
||||
Top.equal(to: .safeArea),
|
||||
],
|
||||
alignment: .top,
|
||||
axis: .vertical
|
||||
), [
|
||||
TextField.node(.init(
|
||||
textFieldStyle,
|
||||
placeholder: "Default",
|
||||
value: text.value,
|
||||
valueHandler: Handler(text.set)
|
||||
)),
|
||||
TextField.node(.init(
|
||||
textFieldStyle,
|
||||
isEnabled: false,
|
||||
placeholder: "Disabled",
|
||||
value: text.value,
|
||||
valueHandler: Handler(text.set)
|
||||
)),
|
||||
TextField.node(.init(
|
||||
textFieldStyle,
|
||||
keyboardAppearance: .dark,
|
||||
placeholder: "Dark",
|
||||
value: text.value,
|
||||
valueHandler: Handler(text.set)
|
||||
)),
|
||||
TextField.node(.init(
|
||||
textFieldStyle,
|
||||
isSecureTextEntry: true,
|
||||
placeholder: "Password",
|
||||
value: text.value,
|
||||
valueHandler: Handler(text.set)
|
||||
)),
|
||||
])
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue