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:
parent
69c563337e
commit
295315e7b0
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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] = []
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 [] }
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
), [
|
||||
])
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)),
|
||||
])
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)),
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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("")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue