import Foundation import SourceKittenFramework import SwiftSyntax struct SyntacticSugarRule: CorrectableRule, SourceKitFreeRule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( identifier: "syntactic_sugar", name: "Syntactic Sugar", description: "Shorthand syntactic sugar should be used, i.e. [Int] instead of Array.", kind: .idiomatic, nonTriggeringExamples: SyntacticSugarRuleExamples.nonTriggering, triggeringExamples: SyntacticSugarRuleExamples.triggering, corrections: SyntacticSugarRuleExamples.corrections ) func validate(file: SwiftLintFile) -> [StyleViolation] { let visitor = SyntacticSugarRuleVisitor(viewMode: .sourceAccurate) return visitor.walk(file: file) { visitor in flattenViolations(visitor.violations) }.map { violation in StyleViolation(ruleDescription: Self.description, severity: configuration.severity, location: Location(file: file, byteOffset: ByteCount(violation.position)), reason: violation.type.violationReason) } } private func flattenViolations(_ violations: [SyntacticSugarRuleViolation]) -> [SyntacticSugarRuleViolation] { violations.flatMap { [$0] + flattenViolations($0.children) } } func correct(file: SwiftLintFile) -> Int { let visitor = SyntacticSugarRuleVisitor(viewMode: .sourceAccurate) return visitor.walk(file: file) { visitor in var context = CorrectingContext(rule: self, file: file, contents: file.contents) context.correctViolations(visitor.violations) file.write(context.contents) return [context.numberOfCorrections] }.reduce(0, +) } } // MARK: - Private private enum SugaredType: String { case optional = "Optional" case array = "Array" case dictionary = "Dictionary" init?(typeName: String) { var typeName = typeName if typeName.hasPrefix("Swift.") { typeName.removeFirst("Swift.".count) } self.init(rawValue: typeName) } var sugaredExample: String { switch self { case .optional: return "Int?" case .array: return "[Int]" case .dictionary: return "[String: Int]" } } var desugaredExample: String { switch self { case .optional, .array: return "\(rawValue)" case .dictionary: return "\(rawValue)" } } var violationReason: String { "Shorthand syntactic sugar should be used, i.e. \(sugaredExample) instead of \(desugaredExample)" } } private struct SyntacticSugarRuleViolation { struct Correction { let typeStart: AbsolutePosition let correction: CorrectionType let leftStart: AbsolutePosition let leftEnd: AbsolutePosition let rightStart: AbsolutePosition let rightEnd: AbsolutePosition } enum CorrectionType { case optional case dictionary(commaStart: AbsolutePosition, commaEnd: AbsolutePosition) case array } let position: AbsolutePosition let type: SugaredType let correction: Correction var children: [Self] = [] } private final class SyntacticSugarRuleVisitor: SyntaxVisitor { var violations: [SyntacticSugarRuleViolation] = [] override func visitPost(_ node: TypeAnnotationSyntax) { // let x: ↓Swift.Optional // let x: ↓Optional if let violation = violation(in: node.type) { violations.append(violation) } } override func visitPost(_ node: FunctionParameterSyntax) { // func x(a: ↓Array, b: Int) -> [Int: Any] if let violation = violation(in: node.type) { violations.append(violation) } } override func visitPost(_ node: ReturnClauseSyntax) { // func x(a: [Int], b: Int) -> ↓Dictionary if let violation = violation(in: node.type) { violations.append(violation) } } override func visitPost(_ node: TypeInitializerClauseSyntax) { // typealias Document = ↓Dictionary if let violation = violation(in: node.value) { violations.append(violation) } } override func visitPost(_ node: AttributedTypeSyntax) { // func x(_ y: inout ↓Array) if let violation = violation(in: node.baseType) { violations.append(violation) } } override func visitPost(_ node: SameTypeRequirementSyntax) { // @_specialize(where S == ↓Array) if let violation = violation(in: node.leftType.as(TypeSyntax.self)) { violations.append(violation) } if let violation = violation(in: node.rightType.as(TypeSyntax.self)) { violations.append(violation) } } override func visitPost(_ node: GenericSpecializationExprSyntax) { // let x = ↓Array.array(of: object) // Skip checks for 'self' or \T Dictionary.self if let parent = node.parent?.as(MemberAccessExprSyntax.self), let lastToken = Array(parent.tokens(viewMode: .sourceAccurate)).last?.tokenKind, [.keyword(.self), .identifier("Type"), .identifier("none"), .identifier("Index")].contains(lastToken) { return } let typeName = node.expression.trimmedDescription if SugaredType(typeName: typeName) != nil { if let violation = violation(from: node) { violations.append(violation) } return } // If there's no type, check all inner generics like in the case of 'Box>' node.genericArgumentClause.arguments .lazy .compactMap { self.violation(in: $0.argument.as(TypeSyntax.self)) } .first .map { violations.append($0) } } override func visitPost(_ node: TypeExprSyntax) { if let violation = violation(in: node.type) { violations.append(violation) } } private func violation(in typeSyntax: TypeSyntax?) -> SyntacticSugarRuleViolation? { if let optionalType = typeSyntax?.as(OptionalTypeSyntax.self) { return violation(in: optionalType.wrappedType) } if let simpleType = typeSyntax?.as(IdentifierTypeSyntax.self) { if SugaredType(typeName: simpleType.name.text) != nil { return violation(from: simpleType) } // If there's no type, check all inner generics like in the case of 'Box>' guard let genericArguments = simpleType.genericArgumentClause else { return nil } let innerTypes = genericArguments.arguments.compactMap { violation(in: $0.argument.as(TypeSyntax.self)) } return innerTypes.first } // Base class is "Swift" for cases like "Swift.Array" if let memberType = typeSyntax?.as(MemberTypeSyntax.self), let baseType = memberType.baseType.as(IdentifierTypeSyntax.self), baseType.name.text == "Swift" { guard SugaredType(typeName: memberType.name.text) != nil else { return nil } return violation(from: memberType) } return nil } private func violation(from node: some SyntaxProtocol & SyntaxWithGenericClause) -> SyntacticSugarRuleViolation? { guard let generic = node.genericArguments, let firstGenericType = generic.arguments.first, let lastGenericType = generic.arguments.last, let typeName = node.typeName, let type = SugaredType(typeName: typeName) else { return nil } let correctionType: SyntacticSugarRuleViolation.CorrectionType switch type { case .optional: correctionType = .optional case .array: correctionType = .array case .dictionary: guard let comma = firstGenericType.trailingComma else { return nil } let lastArgumentEnd = firstGenericType.argument.endPositionBeforeTrailingTrivia correctionType = .dictionary(commaStart: lastArgumentEnd, commaEnd: comma.endPosition) } let firstInnerViolation = violation(in: firstGenericType.argument.as(TypeSyntax.self)) let secondInnerViolation = generic.arguments.count > 1 ? violation(in: lastGenericType.argument.as(TypeSyntax.self)) : nil return SyntacticSugarRuleViolation( position: node.positionAfterSkippingLeadingTrivia, type: type, correction: .init(typeStart: node.position, correction: correctionType, leftStart: generic.leftAngle.position, leftEnd: generic.leftAngle.endPosition, rightStart: lastGenericType.endPositionBeforeTrailingTrivia, rightEnd: generic.rightAngle.endPositionBeforeTrailingTrivia), children: [firstInnerViolation, secondInnerViolation].compactMap { $0 } ) } } private struct CorrectingContext { let rule: R let file: SwiftLintFile var contents: String var numberOfCorrections = 0 mutating func correctViolations(_ violations: [SyntacticSugarRuleViolation]) { let sortedVolations = violations.sorted(by: { $0.correction.typeStart > $1.correction.typeStart }) for violation in sortedVolations { correctViolation(violation) } } mutating func correctViolation(_ violation: SyntacticSugarRuleViolation) { let stringView = file.stringView let correction = violation.correction guard let violationNSRange = stringView.NSRange(start: correction.leftStart, end: correction.rightEnd), file.ruleEnabled(violatingRange: violationNSRange, for: rule) != nil else { return } guard let rightRange = stringView.NSRange(start: correction.rightStart, end: correction.rightEnd), let leftRange = stringView.NSRange(start: correction.typeStart, end: correction.leftEnd) else { return } switch correction.correction { case .array: replaceCharacters(in: rightRange, with: "]") correctViolations(violation.children) replaceCharacters(in: leftRange, with: "[") case let .dictionary(commaStart, commaEnd): replaceCharacters(in: rightRange, with: "]") guard let commaRange = stringView.NSRange(start: commaStart, end: commaEnd) else { return } let violationsAfterComma = violation.children.filter { $0.position > commaStart } correctViolations(violationsAfterComma) replaceCharacters(in: commaRange, with: ": ") let violationsBeforeComma = violation.children.filter { $0.position < commaStart } correctViolations(violationsBeforeComma) replaceCharacters(in: leftRange, with: "[") case .optional where typeIsOpaqueOrExistential(correction: correction): replaceCharacters(in: rightRange, with: ")?") correctViolations(violation.children) replaceCharacters(in: leftRange, with: "(") case .optional: replaceCharacters(in: rightRange, with: "?") correctViolations(violation.children) replaceCharacters(in: leftRange, with: "") } numberOfCorrections += 1 } private func typeIsOpaqueOrExistential(correction: SyntacticSugarRuleViolation.Correction) -> Bool { if let innerTypeRange = file.stringView.NSRange(start: correction.leftEnd, end: correction.rightStart) { let innerTypeString = file.stringView.substring(with: innerTypeRange) return innerTypeString.contains("any ") || innerTypeString.contains("some ") } return false } private mutating func replaceCharacters(in range: NSRange, with replacement: String) { contents = contents.bridge().replacingCharacters(in: range, with: replacement) } } private protocol SyntaxWithGenericClause { var typeName: String? { get } var genericArguments: GenericArgumentClauseSyntax? { get } } extension MemberTypeSyntax: SyntaxWithGenericClause { var typeName: String? { name.text } var genericArguments: GenericArgumentClauseSyntax? { genericArgumentClause } } extension IdentifierTypeSyntax: SyntaxWithGenericClause { var typeName: String? { name.text } var genericArguments: GenericArgumentClauseSyntax? { genericArgumentClause } } extension GenericSpecializationExprSyntax: SyntaxWithGenericClause { var typeName: String? { expression.as(DeclReferenceExprSyntax.self)?.firstToken(viewMode: .sourceAccurate)?.text ?? expression.as(MemberAccessExprSyntax.self)?.declName.baseName.text } var genericArguments: GenericArgumentClauseSyntax? { genericArgumentClause } }