Fix one render function rule (#100)

* Fix getComponents and OneRenderFunctionRule

* Fix RenderGetsHooksRule

* Refactor GetComponents

* Refactor Rules

* Fix RenderGetsHooksRule

* Extend Node children search

* Add tests to RenderGetsHooksRule

* Apply swiftformat

* Fix comments
This commit is contained in:
matvii 2019-05-29 11:56:11 +03:00 committed by GitHub
parent 69c563337e
commit 295315e7b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 307 additions and 89 deletions

View File

@ -1,27 +1,29 @@
//
// GetHookedComponents.swift
// GetComponents.swift
// TokamakLint
//
// Created by Matvii Hodovaniuk on 5/23/19.
// Created by Matvii Hodovaniuk on 5/28/19.
//
import SwiftSyntax
let hookedComponentProtocols = ["CompositeComponent", "LeafComponent"]
let componentProtocols = hookedComponentProtocols + ["PureLeafComponent", "PureComponent"]
extension Node {
/// return Tokamak components that can have hooks in the render
/// placed as a children of node
var hookedComponents: [Node] {
func components(_ protocols: [String]) -> [Node] {
return children(with: "struct")
.compactMap { $0.firstParent(of: SyntaxKind.structDecl.rawValue) }
.filter { structDecl in
let hookedProtocols = ["CompositeComponent", "LeafComponent"]
guard let typeInheritanceClause = structDecl.firstChild(
of: SyntaxKind.typeInheritanceClause.rawValue
) else { return false }
let types = typeInheritanceClause.children(
with: SyntaxKind.simpleTypeIdentifier.rawValue
).compactMap { $0.children.first?.text }
return types.contains { hookedProtocols.contains($0) }
return types.contains { protocols.contains($0) }
}
}
}

View File

@ -38,6 +38,10 @@ final class Node: Equatable {
children.append(node)
}
func children(with type: SyntaxKind) -> [Node] {
return children(with: type.rawValue)
}
func children(with type: String) -> [Node] {
guard children.first != nil else { return [] }
var nodes: [Node] = []

View File

@ -18,8 +18,8 @@ struct HooksRule: Rule {
var violations: [StyleViolation] = []
// search for render function
let structs = visitor.root.hookedComponents
guard !structs.isEmpty else { return [] }
let structs = visitor.root.components(hookedComponentProtocols)
structs.forEach { structDecl in
for render in structDecl.children(with: "render") {
// search for Hooks argument name in the render argument list

View File

@ -16,7 +16,7 @@ struct OneRenderFunctionRule: Rule {
static func validate(visitor: TokenVisitor) -> [StyleViolation] {
do {
let structs = visitor.root.hookedComponents
let structs = visitor.root.components(componentProtocols)
guard !structs.isEmpty else { return [] }

View File

@ -15,45 +15,44 @@ struct RenderGetsHooksRule: Rule {
)
public static func validate(visitor: TokenVisitor) -> [StyleViolation] {
do {
let renderFunction = try visitor.root.getOneRender(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 [] }
var violations: [StyleViolation] = []
let hooksArgument = functionSignature.children(
with: SyntaxKind.simpleTypeIdentifier.rawValue
).filter {
guard let children = $0.children.first else { return false }
return children.text == "Hooks"
}
// search for components declaration
let structs = visitor.root.components(hookedComponentProtocols)
structs.forEach { structDecl in
guard !hooksArgument.isEmpty else {
return [StyleViolation(
ruleDescription: OneRenderFunctionRule.description,
location: Location(
file: visitor.path,
line: renderFunction.range.startRow,
character: renderFunction.range.startColumn
)
)]
// search for render functions
for renderFunction in structDecl.children(with: "render") {
// search for renderCodeBlock
guard let codeBlock = renderFunction.firstParent(
of: SyntaxKind.codeBlockItem
) else { return }
// search for render function signature
guard let functionSignature = codeBlock.firstChild(
of: SyntaxKind.functionSignature
) else { return }
// search for Hooks in render arguments list
let hooksArgument = functionSignature.children(
with: SyntaxKind.simpleTypeIdentifier
).filter { $0.children.first?.text == "Hooks" }
// check if render arguments list contains argument of type Hooks
guard !hooksArgument.isEmpty else {
violations.append(StyleViolation(
ruleDescription: RenderGetsHooksRule.description,
location: Location(
file: visitor.path,
line: renderFunction.range.startRow,
character: renderFunction.range.startColumn
)
))
return
}
}
} catch let error as [StyleViolation] {
return error
} catch {
print(error)
}
return []
return violations
}
}

View File

@ -21,24 +21,30 @@ final class TokamakLintTests: XCTestCase {
XCTAssertEqual(result.count, 1)
}
func testOneRenderFunctionRule() throws {
let path = "\(try srcRoot())/PositiveTestOneRenderFunctionRule.swift"
func testOneRenderFunctionRulePositive() throws {
let path = "\(try srcRoot())/OneRenderFunctionPositive.swift"
let result = try OneRenderFunctionRule.validate(path: path)
XCTAssertEqual(result, [])
}
func testNegativeTestHooksRule() throws {
let path = "\(try srcRoot())/NegativeTestOneRenderFunctionRule.swift"
func testOneRenderFunctionRuleNegative() throws {
let path = "\(try srcRoot())/OneRenderFunctionNegative.swift"
let result = try OneRenderFunctionRule.validate(path: path)
XCTAssertEqual(result, [])
XCTAssertEqual(result.count, 6)
}
func testRenderGetsHooksRule() throws {
let path = "\(try srcRoot())/PositiveTestOneRenderFunctionRule.swift"
func testRenderGetsHooksRulePositive() throws {
let path = "\(try srcRoot())/RenderGetsHooksRulePositive.swift"
let result = try RenderGetsHooksRule.validate(path: path)
XCTAssertEqual(result, [])
}
func testRenderGetsHooksRuleNegative() throws {
let path = "\(try srcRoot())/RenderGetsHooksRuleNegative.swift"
let result = try RenderGetsHooksRule.validate(path: path)
XCTAssertEqual(result.count, 3)
}
func testTwoComponentsCorrectBroken() throws {
let path = "\(try srcRoot())/TwoComponentsCorrectBroken.swift"
let oneRenderFunctionRuleResult = try OneRenderFunctionRule.validate(path: path)

View File

@ -33,8 +33,8 @@
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
A6BFBFEC229D301000F2B06F /* GetComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6BFBFEB229D301000F2B06F /* GetComponents.swift */; };
A6E7BC612293D7B20042E787 /* HooksRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E7BC602293D7B20042E787 /* HooksRule.swift */; };
A6E7BC692296DEDA0042E787 /* GetHookedComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E7BC672296DE9C0042E787 /* GetHookedComponents.swift */; };
OBJ_325 /* ArgumentList.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_239 /* ArgumentList.swift */; };
OBJ_326 /* ArgumentListManipulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_240 /* ArgumentListManipulator.swift */; };
OBJ_327 /* CLI.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_241 /* CLI.swift */; };
@ -399,8 +399,8 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
A6BFBFEB229D301000F2B06F /* GetComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetComponents.swift; sourceTree = "<group>"; };
A6E7BC602293D7B20042E787 /* HooksRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HooksRule.swift; sourceTree = "<group>"; };
A6E7BC672296DE9C0042E787 /* GetHookedComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetHookedComponents.swift; sourceTree = "<group>"; };
OBJ_100 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
OBJ_102 /* BaselineConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaselineConstraint.swift; sourceTree = "<group>"; };
OBJ_103 /* Bottom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bottom.swift; sourceTree = "<group>"; };
@ -855,7 +855,7 @@
isa = PBXGroup;
children = (
OBJ_134 /* GetOneRender.swift */,
A6E7BC672296DE9C0042E787 /* GetHookedComponents.swift */,
A6BFBFEB229D301000F2B06F /* GetComponents.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -1919,8 +1919,8 @@
OBJ_550 /* Location.swift in Sources */,
OBJ_551 /* Node.swift in Sources */,
OBJ_552 /* RuleDescription.swift in Sources */,
A6E7BC692296DEDA0042E787 /* GetHookedComponents.swift in Sources */,
OBJ_553 /* StyleViolation.swift in Sources */,
A6BFBFEC229D301000F2B06F /* GetComponents.swift in Sources */,
OBJ_554 /* TokamakLint.swift in Sources */,
OBJ_555 /* TokenVisitor.swift in Sources */,
A6E7BC612293D7B20042E787 /* HooksRule.swift in Sources */,

View File

@ -1,15 +0,0 @@
import Tokamak
struct TextFieldExample: LeafComponent {
typealias Props = Null
static func render(props: Props, hooks: Hooks) -> AnyNode {
let hookInClosure = hooks.state("")
return StackView.node(.init(
alignment: .top,
axis: .vertical
), [
])
}
}

View File

@ -0,0 +1,171 @@
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)
)),
])
}
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)
)),
])
}
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)
)),
])
}
}
struct TextFieldExample: PureLeafComponent {
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)
)),
])
}
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)
)),
])
}
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)
)),
])
}
}

View File

@ -9,11 +9,8 @@
import Tokamak
struct TextFieldExample: LeafComponent {
typealias Props = Null
struct OneRenderFunctionPositive: PureLeafCOmponent {
static func render(props: Props, hooks: Hooks) -> AnyNode {
let text = hooks.state("")
let textFieldStyle = Style(
[
Height.equal(to: 44),
@ -36,24 +33,31 @@ struct TextFieldExample: LeafComponent {
value: text.value,
valueHandler: Handler(text.set)
)),
])
}
}
struct OneRenderFunctionPositiveAnotherOne: LeafCOmponent {
static func render(props: Props) -> AnyNode {
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,
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",
placeholder: "Default",
value: text.value,
valueHandler: Handler(text.set)
)),

View File

@ -0,0 +1,28 @@
import Tokamak
struct HooksLessLeafComponent: LeafComponent {
static func render(props: Props) -> AnyNode {
let hooks = "Hooks"
let hookMadeWithLove = hooks
}
}
struct HookedLeafComponent: LeafComponent {
static func render(props: Props, hooks: Hooks) -> AnyNode {
let whoPutHookedHere = hooks.state("not me")
}
}
struct HooksLessLeafComponent: LeafComponent {
static func render(props: Props) -> AnyNode {
let hooks = "Hooks"
let hookMadeWithLove = hooks
}
}
struct HooksLessLeafComponent: LeafComponent {
static func render(props: Props) -> AnyNode {
let hooks = "Hooks"
let hookMadeWithLove = hooks
}
}

View File

@ -0,0 +1,19 @@
import Tokamak
struct HookedLeafComponent: LeafComponent {
static func render(props: Props, hooks: Hooks) -> AnyNode {
let hookMadeWithLove = hooks.state("")
}
}
struct AnotherHookedLeafComponent: LeafComponent {
static func render(props: Props, hooks: Hooks) -> AnyNode {
let hookMadeWithLove = hooks.state("")
}
}
struct OneMoreHookedLeafComponent: LeafComponent {
static func render(props: Props, hooks: Hooks) -> AnyNode {
let hookMadeWithLove = hooks.state("")
}
}