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

Use Base Class Rule #84

Merged
merged 11 commits into from Mar 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 20 additions & 2 deletions README.md
Expand Up @@ -74,8 +74,9 @@ Alternatively, if you've installed IBLinter via CocoaPods the script should look
| `enable_autolayout` | Force to use `useAutolayout` option |
| `duplicate_constraint` | Display warning when view has duplicated constraint. |
| `storyboard_viewcontroller_id` | Check that Storyboard ID same as ViewController class name. |
| `image_resources` | Check if image resources are valid. |
| `image_resources` | Check if image resources are valid. |
| `custom_module` | Check if custom class match custom module by `custom_module_rule` config. |
| `use_base_class` | Check if custom class is in base classes by `use_base_class_rule` config. |


Pull requests are encouraged.
Expand All @@ -91,8 +92,9 @@ You can configure IBLinter by adding a `.iblinter.yml` file from project root di
| `enabled_rules` | Enabled rules id. |
| `disabled_rules` | Disabled rules id. |
| `excluded` | Path to ignore for lint. |
| `included` | Path to include for lint. |
| `included` | Path to include for lint. |
| `custom_module_rule` | Custom module rule configs. |
| `use_base_class_rule`| Use base class rule configs.|

### CustomModuleConfig

Expand All @@ -104,6 +106,17 @@ You can configure `custom_module` rule by `CustomModuleConfig` list.
| `included` | Path to `*.swift` classes of the module for `custom_module` lint. |
| `excluded` | Path to ignore for `*.swift` classes of the module for `custom_module` lint. |

### UseBaseClassConfig

You can configure `use_base_class` rule by `UseBaseClassConfig` list.

| key | description |
|:------------------|:---------------------------------- |
| `element_class` | Element class name. |
| `base_classes` | Base classes of the element class. |

**Note:** UseBaseClassRule does not work for classes that inherit base class. You need to add all classes to `base_classes` to check.


```yaml
enabled_rules:
Expand All @@ -121,4 +134,9 @@ custom_module_rule:
- UIComponents/Classes
excluded:
- UIComponents/Classes/Config/Generated
use_base_class_rule:
- element_class: UILabel
base_classes:
- PrimaryLabel
- SecondaryLabel
```
7 changes: 6 additions & 1 deletion Sources/IBLinterKit/Config.swift
Expand Up @@ -14,6 +14,7 @@ public struct Config: Codable {
public let excluded: [String]
public let included: [String]
public let customModuleRule: [CustomModuleConfig]
public let useBaseClassRule: [UseBaseClassConfig]
public let reporter: String

enum CodingKeys: String, CodingKey {
Expand All @@ -22,6 +23,7 @@ public struct Config: Codable {
case excluded = "excluded"
case included = "included"
case customModuleRule = "custom_module_rule"
case useBaseClassRule = "use_base_class_rule"
case reporter = "reporter"
}

Expand All @@ -34,15 +36,17 @@ public struct Config: Codable {
excluded = []
included = []
customModuleRule = []
useBaseClassRule = []
reporter = "xcode"
}

init(disabledRules: [String], enabledRules: [String], excluded: [String], included: [String], customModuleRule: [CustomModuleConfig], reporter: String) {
init(disabledRules: [String], enabledRules: [String], excluded: [String], included: [String], customModuleRule: [CustomModuleConfig], baseClassRule: [UseBaseClassConfig], reporter: String) {
self.disabledRules = disabledRules
self.enabledRules = enabledRules
self.excluded = excluded
self.included = included
self.customModuleRule = customModuleRule
self.useBaseClassRule = baseClassRule
self.reporter = reporter
}

Expand All @@ -53,6 +57,7 @@ public struct Config: Codable {
excluded = try container.decodeIfPresent(Optional<[String]>.self, forKey: .excluded).flatMap { $0 } ?? []
included = try container.decodeIfPresent(Optional<[String]>.self, forKey: .included).flatMap { $0 } ?? []
customModuleRule = try container.decodeIfPresent(Optional<[CustomModuleConfig]>.self, forKey: .customModuleRule).flatMap { $0 } ?? []
useBaseClassRule = try container.decodeIfPresent(Optional<[UseBaseClassConfig]>.self, forKey: .useBaseClassRule)?.flatMap { $0 } ?? []
reporter = try container.decodeIfPresent(Optional<String>.self, forKey: .reporter).flatMap { $0 } ?? "xcode"
}

Expand Down
1 change: 1 addition & 0 deletions Sources/IBLinterKit/Rules/Rule.swift
Expand Up @@ -26,6 +26,7 @@ public struct Rules {
StoryboardViewControllerId.self,
ImageResourcesRule.self,
CustomModuleRule.self,
UseBaseClassRule.self,
AmbiguousViewRule.self,
]
}()
Expand Down
52 changes: 52 additions & 0 deletions Sources/IBLinterKit/Rules/UseBaseClassRule.swift
@@ -0,0 +1,52 @@
//
// UseBaseClassRule.swift
// IBLinterKit
//
// Created by masamichi on 2019/03/07.
//

import Foundation
import IBDecodable

extension Rules {
public struct UseBaseClassRule: Rule {

public static var identifier: String = "use_base_class"

private var baseClasses: [String: [String]] = [:]

public init(context: Context) {
for baseClassConfig in context.config.useBaseClassRule {
self.baseClasses[baseClassConfig.elementClass] = baseClassConfig.baseClasses
}
}

public func validate(storyboard: StoryboardFile) -> [Violation] {
guard let scenes = storyboard.document.scenes else { return [] }
let views = scenes.compactMap { $0.viewController?.viewController.rootView }
return views.flatMap { validate(for: $0, file: storyboard) }
}

public func validate(xib: XibFile) -> [Violation] {
guard let views = xib.document.views else { return [] }
return views.flatMap { validate(for: $0.view, file: xib) }
}

private func validate<T: InterfaceBuilderFile>(for view: ViewProtocol, file: T) -> [Violation] {
let violation: [Violation] = {
guard let baseClassesForElement = baseClasses[view.elementClass] else { return [] }
guard let customClass = view.customClass else {
let message = "CustomClass is not set to \(view.elementClass) (\(view.id)) "
return [Violation(pathString: file.pathString, message: message, level: .warning)]
}

if !baseClassesForElement.contains(customClass) {
let message = "\(customClass) (\(view.id) is not contained in the BaseClasses"
return [Violation(pathString: file.pathString, message: message, level: .warning)]
}
return []
}()
return violation + (view.subviews?.flatMap { validate(for: $0.view, file: file) } ?? [])
}
}
}
29 changes: 29 additions & 0 deletions Sources/IBLinterKit/UseBaseClassConfig.swift
@@ -0,0 +1,29 @@
//
// BaseClassConfig.swift
// AEXML
//
// Created by masamichi on 2019/03/11.
//

import Foundation

public struct UseBaseClassConfig: Codable {
public let elementClass: String
public let baseClasses: [String]

enum CodingKeys: String, CodingKey {
case elementClass = "element_class"
case baseClasses = "base_classes"
}

init(elementClass: String, baseClasses: [String]) {
self.elementClass = elementClass
self.baseClasses = baseClasses
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
elementClass = try container.decode(String.self, forKey: .elementClass)
baseClasses = try container.decodeIfPresent(Optional<[String]>.self, forKey: .baseClasses)?.flatMap { $0 } ?? []
}
}
6 changes: 5 additions & 1 deletion Tests/IBLinterKitTest/Resources/.iblinter.yml
Expand Up @@ -11,4 +11,8 @@ custom_module_rule:
- UIComponents/Classes
excluded:
- UIComponents/Classes/Config/Generated

use_base_class_rule:
- element_class: UILabel
base_classes:
- PrimaryLabel
- SecondaryLabel
44 changes: 44 additions & 0 deletions Tests/IBLinterKitTest/Resources/UseBaseClassTest.xib
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="UILabel" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ToM-Be-eNG">
<rect key="frame" x="158" y="119" width="58" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="PrimaryLabel" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xlx-5v-ehz" customClass="PrimaryLabel">
<rect key="frame" x="137" y="163" width="101" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="SecondaryLabel" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="A8L-Jg-8BD" customClass="SecondaryLabel">
<rect key="frame" x="125" y="216" width="124" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
</view>
</objects>
</document>
17 changes: 13 additions & 4 deletions Tests/IBLinterKitTest/RuleTest.swift
Expand Up @@ -31,23 +31,23 @@ class RuleTest: XCTestCase {

func testDefaultEnabledRules() {
let defaultEnabledRules = Rules.defaultRules.map({ $0.identifier })
let config = Config(disabledRules: [], enabledRules: [], excluded: [], included: [], customModuleRule: [], reporter: "xcode")
let config = Config(disabledRules: [], enabledRules: [], excluded: [], included: [], customModuleRule: [], baseClassRule: [], reporter: "xcode")
let rules = Rules.rules(context(from: config))
XCTAssertEqual(Set(rules.map({ type(of:$0).identifier })), Set(defaultEnabledRules))
XCTAssertEqual(rules.count, defaultEnabledRules.count)
}

func testDisableDefaultEnabledRules() {
let defaultEnabledRules = Rules.defaultRules.map({ $0.identifier })
let config = Config(disabledRules: defaultEnabledRules, enabledRules: [], excluded: [], included: [], customModuleRule: [], reporter: "xcode")
let config = Config(disabledRules: defaultEnabledRules, enabledRules: [], excluded: [], included: [], customModuleRule: [], baseClassRule: [], reporter: "xcode")
let rules = Rules.rules(context(from: config))
XCTAssertEqual(Set(rules.map({ type(of:$0).identifier })), Set())
XCTAssertEqual(rules.count, 0)
}

func testDuplicatedEnabledRules() {
let defaultEnabledRules = Rules.defaultRules.map({ $0.identifier })
let config = Config(disabledRules: [], enabledRules: defaultEnabledRules, excluded: [], included: [], customModuleRule: [], reporter: "xcode")
let config = Config(disabledRules: [], enabledRules: defaultEnabledRules, excluded: [], included: [], customModuleRule: [], baseClassRule: [], reporter: "xcode")
let rules = Rules.rules(context(from: config))
XCTAssertEqual(Set(rules.map({ type(of:$0).identifier })), Set(defaultEnabledRules))
XCTAssertEqual(rules.count, defaultEnabledRules.count)
Expand All @@ -63,7 +63,7 @@ class RuleTest: XCTestCase {

func testCustomModule() {
let defaultEnabledRules = Rules.defaultRules.map({ $0.identifier })
let config = Config(disabledRules: defaultEnabledRules, enabledRules: ["custom_module"], excluded: [], included: [], customModuleRule: [CustomModuleConfig(module: "TestCustomModule", included: ["Tests/IBLinterKitTest/Resources/TestCustomModule"], excluded: ["Tests/IBLinterKitTest/Resources/TestCustomModule/CustomModuleExcluded"])], reporter: "xcode")
let config = Config(disabledRules: defaultEnabledRules, enabledRules: ["custom_module"], excluded: [], included: [], customModuleRule: [CustomModuleConfig(module: "TestCustomModule", included: ["Tests/IBLinterKitTest/Resources/TestCustomModule"], excluded: ["Tests/IBLinterKitTest/Resources/TestCustomModule/CustomModuleExcluded"])], baseClassRule: [], reporter: "xcode")
let rules = Rules.rules(context(from: config))
XCTAssertEqual(Set(rules.map({ type(of:$0).identifier })), Set(["custom_module"]))
let rule = rules[0]
Expand All @@ -81,6 +81,15 @@ class RuleTest: XCTestCase {
let violations = try! rule.validate(storyboard: StoryboardFile(url: url))
XCTAssertEqual(violations.count, 2)
}

func testUseBaseClass() {
let url = self.url(forResource: "UseBaseClassTest", withExtension: "xib")
let defaultEnabledRules = Rules.defaultRules.map({ $0.identifier })
let config = Config(disabledRules: defaultEnabledRules, enabledRules: [], excluded: [], included: [], customModuleRule: [], baseClassRule: [UseBaseClassConfig(elementClass: "UILabel", baseClasses: ["PrimaryLabel", "SecondaryLabel"])], reporter: "xcode")
let rule = Rules.UseBaseClassRule(context: context(from: config))
let violations = try! rule.validate(xib: XibFile(url: url))
XCTAssertEqual(violations.count, 1)
}
}

// MARK: resource utils
Expand Down
6 changes: 3 additions & 3 deletions Tests/IBLinterTest/Config+LintablePathsTests.swift
Expand Up @@ -9,7 +9,7 @@ class ConfigLintablePathsTests: XCTestCase {
let config = Config(
disabledRules: [], enabledRules: [],
excluded: ["Level1_1"], included: [],
customModuleRule: [], reporter: ""
customModuleRule: [], baseClassRule: [], reporter: ""
)
let projectPath = bundleURL.appendingPathComponent("ProjectMock")
let lintablePaths = config.lintablePaths(workDirectory: projectPath, fileExtension: "xib")
Expand All @@ -24,7 +24,7 @@ class ConfigLintablePathsTests: XCTestCase {
let config = Config(
disabledRules: [], enabledRules: [],
excluded: [], included: ["Level1_2"],
customModuleRule: [], reporter: ""
customModuleRule: [], baseClassRule: [], reporter: ""
)
let projectPath = bundleURL.appendingPathComponent("ProjectMock")
let lintablePaths = config.lintablePaths(workDirectory: projectPath, fileExtension: "xib")
Expand All @@ -39,7 +39,7 @@ class ConfigLintablePathsTests: XCTestCase {
let config = Config(
disabledRules: [], enabledRules: [],
excluded: ["Level1_1"], included: ["Level1_1/Level2_1"],
customModuleRule: [], reporter: ""
customModuleRule: [], baseClassRule: [], reporter: ""
)
let projectPath = bundleURL.appendingPathComponent("ProjectMock")
let lintablePaths = config.lintablePaths(workDirectory: projectPath, fileExtension: "xib")
Expand Down