mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
318 lines
11 KiB
Swift
318 lines
11 KiB
Swift
import SwiftSyntax
|
|
|
|
@SwiftSyntaxRule(foldExpressions: true, optIn: true)
|
|
struct NoMagicNumbersRule: Rule {
|
|
var configuration = NoMagicNumbersConfiguration()
|
|
|
|
static let description = RuleDescription(
|
|
identifier: "no_magic_numbers",
|
|
name: "No Magic Numbers",
|
|
description: "Magic numbers should be replaced by named constants",
|
|
kind: .idiomatic,
|
|
nonTriggeringExamples: [
|
|
Example("var foo = 123"),
|
|
Example("static let bar: Double = 0.123"),
|
|
Example("let a = b + 1.0"),
|
|
Example("array[0] + array[1] "),
|
|
Example("let foo = 1_000.000_01"),
|
|
Example("// array[1337]"),
|
|
Example("baz(\"9999\")"),
|
|
Example("""
|
|
func foo() {
|
|
let x: Int = 2
|
|
let y = 3
|
|
let vector = [x, y, -1]
|
|
}
|
|
"""),
|
|
Example("""
|
|
class A {
|
|
var foo: Double = 132
|
|
static let bar: Double = 0.98
|
|
}
|
|
"""),
|
|
Example("""
|
|
@available(iOS 13, *)
|
|
func version() {
|
|
if #available(iOS 13, OSX 10.10, *) {
|
|
return
|
|
}
|
|
}
|
|
"""),
|
|
Example("""
|
|
enum Example: Int {
|
|
case positive = 2
|
|
case negative = -2
|
|
}
|
|
"""),
|
|
Example("""
|
|
class FooTests: XCTestCase {
|
|
let array: [Int] = []
|
|
let bar = array[42]
|
|
}
|
|
"""),
|
|
Example("""
|
|
class FooTests: XCTestCase {
|
|
class Bar {
|
|
let array: [Int] = []
|
|
let bar = array[42]
|
|
}
|
|
}
|
|
"""),
|
|
Example("""
|
|
class MyTest: XCTestCase {}
|
|
extension MyTest {
|
|
let a = Int(3)
|
|
}
|
|
"""),
|
|
Example("""
|
|
extension MyTest {
|
|
let a = Int(3)
|
|
}
|
|
class MyTest: XCTestCase {}
|
|
"""),
|
|
Example("let foo = 1 << 2"),
|
|
Example("let foo = 1 >> 2"),
|
|
Example("let foo = 2 >> 2"),
|
|
Example("let foo = 2 << 2"),
|
|
Example("let a = b / 100.0"),
|
|
Example("let range = 2 ..< 12"),
|
|
Example("let range = ...12"),
|
|
Example("let range = 12..."),
|
|
Example("let (lowerBound, upperBound) = (400, 599)"),
|
|
Example("let a = (5, 10)"),
|
|
Example("let notFound = (statusCode: 404, description: \"Not Found\", isError: true)"),
|
|
Example("#Preview { ContentView(value: 5) }"),
|
|
Example("@Test func f() { let _ = 2 + 2 }"),
|
|
Example("""
|
|
@Suite struct Test {
|
|
@Test func f() {
|
|
func g() { let _ = 2 + 2 }
|
|
let _ = 2 + 2
|
|
}
|
|
}
|
|
"""),
|
|
Example("""
|
|
@Suite actor Test {
|
|
private var a: Int { 2 }
|
|
@Test func f() { let _ = 2 + a }
|
|
}
|
|
"""),
|
|
Example("""
|
|
class Test { // @Suite implicitly
|
|
private var a: Int { 2 }
|
|
@Test func f() { let _ = 2 + a }
|
|
}
|
|
"""),
|
|
],
|
|
triggeringExamples: [
|
|
Example("foo(↓321)"),
|
|
Example("bar(↓1_000.005_01)"),
|
|
Example("array[↓42]"),
|
|
Example("let box = array[↓12 + ↓14]"),
|
|
Example("let a = b + ↓2.0"),
|
|
Example("let range = 2 ... ↓12 + 1"),
|
|
Example("let range = ↓2*↓6..."),
|
|
Example("let slice = array[↓2...↓4]"),
|
|
Example("for i in ↓3 ..< ↓8 {}"),
|
|
Example("let n: Int = Int(r * ↓255) << ↓16 | Int(g * ↓255) << ↓8"),
|
|
Example("Color.primary.opacity(isAnimate ? ↓0.1 : ↓1.5)"),
|
|
Example("""
|
|
class MyTest: XCTestCase {}
|
|
extension NSObject {
|
|
let a = Int(↓3)
|
|
}
|
|
"""),
|
|
Example("""
|
|
if (fileSize > ↓1000000) {
|
|
return
|
|
}
|
|
"""),
|
|
Example("let imageHeight = (width - ↓24)"),
|
|
Example("return (↓5, ↓10, ↓15)"),
|
|
Example("""
|
|
#ExampleMacro {
|
|
ContentView(value: ↓5)
|
|
}
|
|
"""),
|
|
]
|
|
)
|
|
}
|
|
|
|
private extension NoMagicNumbersRule {
|
|
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
|
|
private var testClasses: Set<String> = []
|
|
private var nonTestClasses: Set<String> = []
|
|
private var possibleViolations: [String: Set<AbsolutePosition>] = [:]
|
|
|
|
override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
node.isTestSuite ? .skipChildren : .visitChildren
|
|
}
|
|
|
|
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
node.isTestSuite ? .skipChildren : .visitChildren
|
|
}
|
|
|
|
override func visitPost(_ node: ClassDeclSyntax) {
|
|
let className = node.name.text
|
|
if node.isXCTestCase(configuration.testParentClasses) {
|
|
testClasses.insert(className)
|
|
removeViolations(forClassName: className)
|
|
} else {
|
|
nonTestClasses.insert(className)
|
|
}
|
|
}
|
|
|
|
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
node.isTestSuite ? .skipChildren : .visitChildren
|
|
}
|
|
|
|
override func visitPost(_ node: FloatLiteralExprSyntax) {
|
|
guard node.literal.isMagicNumber else {
|
|
return
|
|
}
|
|
collectViolation(forNode: node)
|
|
}
|
|
|
|
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
node.attributes.contains(attributeNamed: "Test") ? .skipChildren : .visitChildren
|
|
}
|
|
|
|
override func visitPost(_ node: IntegerLiteralExprSyntax) {
|
|
guard node.literal.isMagicNumber else {
|
|
return
|
|
}
|
|
collectViolation(forNode: node)
|
|
}
|
|
|
|
override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind {
|
|
node.macroName.text == "Preview" ? .skipChildren : .visitChildren
|
|
}
|
|
|
|
override func visit(_ node: PatternBindingSyntax) -> SyntaxVisitorContinueKind {
|
|
node.isSimpleTupleAssignment ? .skipChildren : .visitChildren
|
|
}
|
|
|
|
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
node.isTestSuite ? .skipChildren : .visitChildren
|
|
}
|
|
|
|
private func collectViolation(forNode node: some ExprSyntaxProtocol) {
|
|
if node.isMemberOfATestClass(configuration.testParentClasses) {
|
|
return
|
|
}
|
|
if node.isOperandOfFreestandingShiftOperation() {
|
|
return
|
|
}
|
|
let violation = node.positionAfterSkippingLeadingTrivia
|
|
if let extendedTypeName = node.extendedTypeName() {
|
|
if !testClasses.contains(extendedTypeName) {
|
|
violations.append(violation)
|
|
if !nonTestClasses.contains(extendedTypeName) {
|
|
possibleViolations[extendedTypeName, default: []].insert(violation)
|
|
}
|
|
}
|
|
} else {
|
|
violations.append(violation)
|
|
}
|
|
}
|
|
|
|
private func removeViolations(forClassName className: String) {
|
|
guard let possibleViolationsForClass = possibleViolations[className] else {
|
|
return
|
|
}
|
|
let violationsToRemove = Set(possibleViolationsForClass.map { ReasonedRuleViolation(position: $0) })
|
|
violations.removeAll { violationsToRemove.contains($0) }
|
|
possibleViolations.removeValue(forKey: className)
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension DeclGroupSyntax {
|
|
var isTestSuite: Bool {
|
|
if attributes.contains(attributeNamed: "Suite") {
|
|
return true
|
|
}
|
|
return memberBlock.members.contains {
|
|
$0.decl.as(FunctionDeclSyntax.self)?.attributes.contains(attributeNamed: "Test") == true
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension TokenSyntax {
|
|
var isMagicNumber: Bool {
|
|
guard let number = Double(text.replacingOccurrences(of: "_", with: "")) else {
|
|
return false
|
|
}
|
|
if [0, 1, 100].contains(number) {
|
|
return false
|
|
}
|
|
guard let grandparent = parent?.parent else {
|
|
return true
|
|
}
|
|
if grandparent.is(InitializerClauseSyntax.self) {
|
|
return false
|
|
}
|
|
let operatorParent = grandparent.as(PrefixOperatorExprSyntax.self)?.parent
|
|
?? grandparent.as(PostfixOperatorExprSyntax.self)?.parent
|
|
?? grandparent.asAcceptedInfixOperator?.parent
|
|
return operatorParent?.is(InitializerClauseSyntax.self) != true
|
|
}
|
|
}
|
|
|
|
private extension Syntax {
|
|
var asAcceptedInfixOperator: InfixOperatorExprSyntax? {
|
|
if let infixOp = `as`(InfixOperatorExprSyntax.self),
|
|
let operatorSymbol = infixOp.operator.as(BinaryOperatorExprSyntax.self)?.operator.tokenKind,
|
|
[.binaryOperator("..."), .binaryOperator("..<")].contains(operatorSymbol) {
|
|
return infixOp
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private extension ExprSyntaxProtocol {
|
|
func isMemberOfATestClass(_ testParentClasses: Set<String>) -> Bool {
|
|
var parent = parent
|
|
while parent != nil {
|
|
if
|
|
let classDecl = parent?.as(ClassDeclSyntax.self),
|
|
classDecl.isXCTestCase(testParentClasses) {
|
|
return true
|
|
}
|
|
parent = parent?.parent
|
|
}
|
|
return false
|
|
}
|
|
|
|
func extendedTypeName() -> String? {
|
|
var parent = parent
|
|
while parent != nil {
|
|
if let extensionDecl = parent?.as(ExtensionDeclSyntax.self) {
|
|
return extensionDecl.extendedType.trimmedDescription
|
|
}
|
|
parent = parent?.parent
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isOperandOfFreestandingShiftOperation() -> Bool {
|
|
if let operation = parent?.as(InfixOperatorExprSyntax.self),
|
|
let operatorSymbol = operation.operator.as(BinaryOperatorExprSyntax.self)?.operator.tokenKind,
|
|
[.binaryOperator("<<"), .binaryOperator(">>")].contains(operatorSymbol) {
|
|
return operation.parent?.isProtocol((any ExprSyntaxProtocol).self) != true
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
private extension PatternBindingSyntax {
|
|
var isSimpleTupleAssignment: Bool {
|
|
initializer?.value.as(TupleExprSyntax.self)?.elements.allSatisfy {
|
|
$0.expression.is(IntegerLiteralExprSyntax.self) ||
|
|
$0.expression.is(FloatLiteralExprSyntax.self) ||
|
|
$0.expression.is(StringLiteralExprSyntax.self) ||
|
|
$0.expression.is(BooleanLiteralExprSyntax.self)
|
|
} ?? false
|
|
}
|
|
}
|