SwiftLint/Source/SwiftLintFramework/Extensions/SwiftSyntax+SwiftLint.swift

294 lines
7.8 KiB
Swift

import Foundation
import SwiftSyntax
// workaround for https://bugs.swift.org/browse/SR-10121 so we can use `Self` in a closure
protocol SwiftLintSyntaxVisitor: SyntaxVisitor {}
extension SyntaxVisitor: SwiftLintSyntaxVisitor {}
extension SwiftLintSyntaxVisitor {
func walk<T, SyntaxType: SyntaxProtocol>(tree: SyntaxType, handler: (Self) -> T) -> T {
#if DEBUG
// workaround for stack overflow when running in debug
// https://bugs.swift.org/browse/SR-11170
let lock = NSLock()
let work = DispatchWorkItem {
lock.lock()
self.walk(tree)
lock.unlock()
}
let thread = Thread {
work.perform()
}
thread.stackSize = 8 << 20 // 8 MB.
thread.start()
work.wait()
lock.lock()
defer {
lock.unlock()
}
return handler(self)
#else
walk(tree)
return handler(self)
#endif
}
func walk<T>(file: SwiftLintFile, handler: (Self) -> [T]) -> [T] {
let syntaxTree = file.syntaxTree
return walk(tree: syntaxTree, handler: handler)
}
}
extension SyntaxProtocol {
func windowsOfThreeTokens() -> [(TokenSyntax, TokenSyntax, TokenSyntax)] {
Array(tokens(viewMode: .sourceAccurate))
.windows(ofCount: 3)
.map { tokens in
let previous = tokens[tokens.startIndex]
let current = tokens[tokens.startIndex + 1]
let next = tokens[tokens.startIndex + 2]
return (previous, current, next)
}
}
func isContainedIn(regions: [SourceRange], locationConverter: SourceLocationConverter) -> Bool {
positionAfterSkippingLeadingTrivia.isContainedIn(regions: regions, locationConverter: locationConverter)
}
}
extension AbsolutePosition {
func isContainedIn(regions: [SourceRange], locationConverter: SourceLocationConverter) -> Bool {
regions.contains { region in
region.contains(self, locationConverter: locationConverter)
}
}
}
extension ExprSyntax {
var asFunctionCall: FunctionCallExprSyntax? {
if let functionCall = self.as(FunctionCallExprSyntax.self) {
return functionCall
} else if let tuple = self.as(TupleExprSyntax.self),
tuple.elementList.count == 1,
let firstElement = tuple.elementList.first,
let functionCall = firstElement.expression.as(FunctionCallExprSyntax.self) {
return functionCall
} else {
return nil
}
}
}
extension StringLiteralExprSyntax {
var isEmptyString: Bool {
segments.count == 1 && segments.first?.contentLength == .zero
}
}
extension TokenKind {
var isEqualityComparison: Bool {
self == .spacedBinaryOperator("==") ||
self == .spacedBinaryOperator("!=") ||
self == .unspacedBinaryOperator("==")
}
}
extension ModifierListSyntax? {
var containsLazy: Bool {
contains(tokenKind: .contextualKeyword("lazy"))
}
var containsOverride: Bool {
contains(tokenKind: .contextualKeyword("override"))
}
var containsStaticOrClass: Bool {
isStatic || isClass
}
var isStatic: Bool {
contains(tokenKind: .staticKeyword)
}
var isClass: Bool {
contains(tokenKind: .classKeyword)
}
var isPrivateOrFileprivate: Bool {
guard let modifiers = self else {
return false
}
return modifiers.contains { elem in
(elem.name.tokenKind == .privateKeyword || elem.name.tokenKind == .fileprivateKeyword) &&
elem.detail == nil
}
}
private func contains(tokenKind: TokenKind) -> Bool {
guard let modifiers = self else {
return false
}
return modifiers.contains { $0.name.tokenKind == tokenKind }
}
}
extension VariableDeclSyntax {
var isIBOutlet: Bool {
attributes?.contains { attr in
attr.as(AttributeSyntax.self)?.attributeName.tokenKind == .identifier("IBOutlet")
} ?? false
}
var weakOrUnownedModifier: DeclModifierSyntax? {
modifiers?.first { decl in
decl.name.tokenKind == .contextualKeyword("weak") ||
decl.name.tokenKind == .contextualKeyword("unowned")
}
}
var isInstanceVariable: Bool {
!modifiers.containsStaticOrClass
}
}
extension FunctionDeclSyntax {
var isIBAction: Bool {
attributes?.contains { attr in
attr.as(AttributeSyntax.self)?.attributeName.tokenKind == .identifier("IBAction")
} ?? false
}
/// Returns the signature including arguments, e.g "setEditing(_:animated:)"
func resolvedName() -> String {
var name = self.identifier.text
name += "("
let params = signature.input.parameterList.compactMap { param in
(param.firstName ?? param.secondName)?.text.appending(":")
}
name += params.joined()
name += ")"
return name
}
/// How many times this function calls the `super` implementation in its body.
/// Returns 0 if the function has no body.
func numberOfCallsToSuper() -> Int {
guard let body = body else {
return 0
}
return SuperCallVisitor(expectedFunctionName: identifier.text)
.walk(tree: body, handler: \.superCallsCount)
}
var isQuickSpecFunction: Bool {
return identifier.tokenKind == .identifier("spec") &&
signature.input.parameterList.isEmpty &&
modifiers.containsOverride
}
}
extension AccessorBlockSyntax {
var getAccessor: AccessorDeclSyntax? {
accessors.first { accessor in
accessor.accessorKind.tokenKind == .contextualKeyword("get")
}
}
var setAccessor: AccessorDeclSyntax? {
accessors.first { accessor in
accessor.accessorKind.tokenKind == .contextualKeyword("set")
}
}
}
extension Trivia {
func containsNewlines() -> Bool {
contains { piece in
if case .newlines = piece {
return true
} else {
return false
}
}
}
var isSingleSpace: Bool {
self == .spaces(1)
}
}
extension ClassDeclSyntax {
var containsInheritance: Bool {
guard let inheritanceList = inheritanceClause?.inheritedTypeCollection else {
return false
}
return inheritanceList.isNotEmpty
}
}
extension IntegerLiteralExprSyntax {
var isZero: Bool {
guard case let .integerLiteral(number) = digits.tokenKind else {
return false
}
return number.isZero
}
}
extension FloatLiteralExprSyntax {
var isZero: Bool {
guard case let .floatingLiteral(number) = floatingDigits.tokenKind else {
return false
}
return number.isZero
}
}
private extension String {
var isZero: Bool {
if self == "0" { // fast path
return true
}
var number = lowercased()
for prefix in ["0x", "0o", "0b"] {
number = number.deletingPrefix(prefix)
}
number = number.replacingOccurrences(of: "_", with: "")
return Float(number) == 0
}
}
private class SuperCallVisitor: SyntaxVisitor {
private let expectedFunctionName: String
private(set) var superCallsCount = 0
init(expectedFunctionName: String) {
self.expectedFunctionName = expectedFunctionName
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: FunctionCallExprSyntax) {
guard let expr = node.calledExpression.as(MemberAccessExprSyntax.self),
expr.base?.as(SuperRefExprSyntax.self) != nil,
expr.name.text == expectedFunctionName else {
return
}
superCallsCount += 1
}
}