diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c81bcd1ff..70ee743219 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,9 @@ [braker1nine](https://github.com/braker1nine) [#5641](https://github.com/realm/SwiftLint/issues/5641) +* Support deinitializers and subscripts in `function_body_length` rule. + [SimplyDanny](https://github.com/SimplyDanny) + ### Bug Fixes * Improved error reporting when SwiftLint exits, because of an invalid configuration file diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift index e5df6e56df..60930c44f8 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift @@ -4,16 +4,118 @@ import SwiftSyntax struct FunctionBodyLengthRule: Rule { var configuration = SeverityLevelsConfiguration(warning: 50, error: 100) + private static let testConfig = ["warning": 2] + static let description = RuleDescription( identifier: "function_body_length", name: "Function Body Length", description: "Function bodies should not span too many lines", - kind: .metrics + kind: .metrics, + nonTriggeringExamples: [ + Example("func f() {}", configuration: Self.testConfig), + Example(""" + func f() { + let x = 0 + } + """, configuration: Self.testConfig), + Example(""" + func f() { + let x = 0 + let y = 1 + } + """, configuration: Self.testConfig), + Example(""" + func f() { + let x = 0 + // comments + // will + // be + // ignored + } + """, configuration: Self.testConfig), + Example(""" + func f() { + let x = 0 + // empty lines will be ignored + + + } + """, configuration: Self.testConfig), + ], + + triggeringExamples: [ + Example(""" + ↓func f() { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: Self.testConfig), + Example(""" + class C { + ↓deinit { + let x = 0 + let y = 1 + let z = 2 + } + } + """, configuration: Self.testConfig), + Example(""" + class C { + ↓init() { + let x = 0 + let y = 1 + let z = 2 + } + } + """, configuration: Self.testConfig), + Example(""" + class C { + ↓subscript() -> Int { + let x = 0 + let y = 1 + return x + y + } + } + """, configuration: Self.testConfig), + Example(""" + struct S { + subscript() -> Int { + ↓get { + let x = 0 + let y = 1 + return x + y + } + ↓set { + let x = 0 + let y = 1 + let z = 2 + } + ↓willSet { + let x = 0 + let y = 1 + let z = 2 + } + } + } + """, configuration: Self.testConfig), + ] ) } private extension FunctionBodyLengthRule { final class Visitor: BodyLengthVisitor { + override func visitPost(_ node: DeinitializerDeclSyntax) { + if let body = node.body { + registerViolations( + leftBrace: body.leftBrace, + rightBrace: body.rightBrace, + violationNode: node.deinitKeyword, + objectName: "Deinitializer" + ) + } + } + override func visitPost(_ node: FunctionDeclSyntax) { if let body = node.body { registerViolations( @@ -35,5 +137,32 @@ private extension FunctionBodyLengthRule { ) } } + + override func visitPost(_ node: SubscriptDeclSyntax) { + guard let body = node.accessorBlock else { + return + } + if case .getter = body.accessors { + registerViolations( + leftBrace: body.leftBrace, + rightBrace: body.rightBrace, + violationNode: node.subscriptKeyword, + objectName: "Subscript" + ) + } + if case let .accessors(accessors) = body.accessors { + for accessor in accessors { + guard let body = accessor.body else { + continue + } + registerViolations( + leftBrace: body.leftBrace, + rightBrace: body.rightBrace, + violationNode: accessor.accessorSpecifier, + objectName: "Accessor" + ) + } + } + } } } diff --git a/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift b/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift index ba6601d8f1..41422a8dd1 100644 --- a/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift +++ b/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift @@ -2,102 +2,67 @@ import TestHelpers import XCTest -private func funcWithBody(_ body: String, - violates: Bool = false, - file: StaticString = #filePath, - line: UInt = #line) -> Example { - let marker = violates ? "↓" : "" - return Example("func \(marker)abc() {\n\(body)}\n", file: file, line: line) -} - -private func violatingFuncWithBody(_ body: String, file: StaticString = #filePath, line: UInt = #line) -> Example { - funcWithBody(body, violates: true, file: file, line: line) -} - final class FunctionBodyLengthRuleTests: SwiftLintTestCase { - func testFunctionBodyLengths() { - let longFunctionBody = funcWithBody(repeatElement("x = 0\n", count: 49).joined()) - XCTAssertEqual(self.violations(longFunctionBody), []) + func testWarning() { + let example = Example(""" + func f() { + let x = 0 + let y = 1 + let z = 2 + } + """) - let longerFunctionBody = violatingFuncWithBody(repeatElement("x = 0\n", count: 51).joined()) XCTAssertEqual( - self.violations(longerFunctionBody), + self.violations(example, configuration: ["warning": 2, "error": 4]), [ StyleViolation( ruleDescription: FunctionBodyLengthRule.description, + severity: .warning, location: Location(file: nil, line: 1, character: 1), - reason: "Function body should span 50 lines or less excluding comments and " + - "whitespace: currently spans 51 lines" + reason: """ + Function body should span 2 lines or less excluding comments and \ + whitespace: currently spans 3 lines + """ ), ] ) - - let longerFunctionBodyWithEmptyLines = funcWithBody( - repeatElement("\n", count: 100).joined() - ) - XCTAssertEqual(self.violations(longerFunctionBodyWithEmptyLines), []) } - func testFunctionBodyLengthsWithComments() { - let longFunctionBodyWithComments = funcWithBody( - repeatElement("x = 0\n", count: 49).joined() + - "// comment only line should be ignored.\n" - ) - XCTAssertEqual(violations(longFunctionBodyWithComments), []) + func testError() { + let example = Example(""" + func f() { + let x = 0 + let y = 1 + let z = 2 + } + """) - let longerFunctionBodyWithComments = violatingFuncWithBody( - repeatElement("x = 0\n", count: 51).joined() + - "// comment only line should be ignored.\n" - ) XCTAssertEqual( - self.violations(longerFunctionBodyWithComments), + self.violations(example, configuration: ["warning": 1, "error": 2]), [ StyleViolation( ruleDescription: FunctionBodyLengthRule.description, + severity: .error, location: Location(file: nil, line: 1, character: 1), - reason: "Function body should span 50 lines or less excluding comments and " + - "whitespace: currently spans 51 lines" + reason: """ + Function body should span 2 lines or less excluding comments and \ + whitespace: currently spans 3 lines + """ ), ] ) } - func testFunctionBodyLengthsWithMultilineComments() { - let longFunctionBodyWithMultilineComments = funcWithBody( - repeatElement("x = 0\n", count: 49).joined() + - "/* multi line comment only line should be ignored.\n*/\n" - ) - XCTAssertEqual(self.violations(longFunctionBodyWithMultilineComments), []) + func testViolationMessages() { + let types = FunctionBodyLengthRule.description.triggeringExamples.flatMap { + self.violations($0, configuration: ["warning": 2]) + }.compactMap { + $0.reason.split(separator: " ", maxSplits: 1).first + } - let longerFunctionBodyWithMultilineComments = violatingFuncWithBody( - repeatElement("x = 0\n", count: 51).joined() + - "/* multi line comment only line should be ignored.\n*/\n" - ) - XCTAssertEqual( - self.violations(longerFunctionBodyWithMultilineComments), - [ - StyleViolation( - ruleDescription: FunctionBodyLengthRule.description, - location: Location(file: nil, line: 1, character: 1), - reason: "Function body should span 50 lines or less excluding comments and " + - "whitespace: currently spans 51 lines" - ), - ] - ) - } - - func testConfiguration() { - let function = violatingFuncWithBody(repeatElement("x = 0\n", count: 10).joined()) - - XCTAssertEqual(self.violations(function, configuration: ["warning": 12]).count, 0) - XCTAssertEqual(self.violations(function, configuration: ["warning": 12, "error": 14]).count, 0) - XCTAssertEqual( - self.violations(function, configuration: ["warning": 8]).map(\.reason), - ["Function body should span 8 lines or less excluding comments and whitespace: currently spans 10 lines"] - ) XCTAssertEqual( - self.violations(function, configuration: ["warning": 12, "error": 8]).map(\.reason), - ["Function body should span 8 lines or less excluding comments and whitespace: currently spans 10 lines"] + types, + ["Function", "Deinitializer", "Initializer", "Subscript", "Accessor", "Accessor", "Accessor"] ) }