Skip to content

Commit

Permalink
Automatic code removal, closes #666
Browse files Browse the repository at this point in the history
  • Loading branch information
ileitch committed Jan 21, 2024
1 parent 432ec21 commit 1dc4bc4
Show file tree
Hide file tree
Showing 43 changed files with 1,362 additions and 9 deletions.
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/tuist/xcodeproj",
"state" : {
"revision" : "447c159b0c5fb047a024fd8d942d4a76cf47dde0",
"version" : "8.16.0"
"revision" : "a3e5d54f8c8a2964ee54870fda33b28651416581",
"version" : "8.17.0"
}
},
{
Expand Down
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ var targets: [PackageDescription.Target] = [
],
path: "Tests/Fixtures/RetentionFixtures"
),
.target(
name: "RemovalFixtures",
path: "Tests/Fixtures/RemovalFixtures"
),
.target(
name: "UnusedParameterFixtures",
path: "Tests/Fixtures/UnusedParameterFixtures",
Expand Down
5 changes: 5 additions & 0 deletions Sources/Frontend/Commands/ScanBehavior.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ final class ScanBehavior {
results = try block(project)
let interval = logger.beginInterval("result:output")
let filteredResults = OutputDeclarationFilter().filter(results)

if configuration.autoRemove {
try ScanResultRemover().remove(results: filteredResults)
}

let output = try configuration.outputFormat.formatter.init(configuration: configuration).format(filteredResults)

if configuration.outputFormat.supportsAuxiliaryOutput {
Expand Down
4 changes: 4 additions & 0 deletions Sources/Frontend/Commands/ScanCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ struct ScanCommand: FrontendCommand {
@Flag(help: "Retain properties on Codable types")
var retainCodableProperties: Bool = defaultConfiguration.$retainCodableProperties.defaultValue

@Flag(help: "Automatically remove code that can be done so safely without introducing build errors (experimental)")
var autoRemove: Bool = defaultConfiguration.$autoRemove.defaultValue

@Flag(help: "Clean existing build artifacts before building")
var cleanBuild: Bool = defaultConfiguration.$cleanBuild.defaultValue

Expand Down Expand Up @@ -148,6 +151,7 @@ struct ScanCommand: FrontendCommand {
configuration.apply(\.$externalEncodableProtocols, externalEncodableProtocols)
configuration.apply(\.$externalCodableProtocols, externalCodableProtocols)
configuration.apply(\.$externalTestCaseClasses, externalTestCaseClasses)
configuration.apply(\.$autoRemove, autoRemove)
configuration.apply(\.$verbose, verbose)
configuration.apply(\.$quiet, quiet)
configuration.apply(\.$disableUpdateCheck, disableUpdateCheck)
Expand Down
4 changes: 2 additions & 2 deletions Sources/Frontend/Scan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ final class Scan {
logger.endInterval(analyzeInterval)

let resultInterval = logger.beginInterval("result:build")
let result = ScanResultBuilder.build(for: graph)
let results = ScanResultBuilder.build(for: graph)
logger.endInterval(resultInterval)

return result
return results
}
}
35 changes: 35 additions & 0 deletions Sources/PeripheryKit/CodeRemoval/EmptyExtensionSyntaxRemover.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation
import Foundation
import SwiftParser
import SwiftSyntax
import SystemPackage

final class EmptyExtensionSyntaxRemover: SyntaxRewriter, TriviaSplitting {
private let locationBuilder: SourceLocationBuilder

init(locationBuilder: SourceLocationBuilder) {
self.locationBuilder = locationBuilder
}

func perform(syntax: SourceFileSyntax) -> SourceFileSyntax {
visit(syntax)
}

override func visit(_ node: CodeBlockItemListSyntax) -> CodeBlockItemListSyntax {
let newChildren = node.compactMap { child -> CodeBlockItemSyntax? in
guard let extDecl = child.item.as(ExtensionDeclSyntax.self) else { return child }

let members = extDecl.memberBlock.members
let hasMembers = !(members.count == 0 || (members.count == 1 && members.first?.decl.is(MissingDeclSyntax.self) ?? false))
let hasInheritance = extDecl.inheritanceClause != nil

if !hasMembers, !hasInheritance {
return remainingTriviaDecl(from: child.item.leadingTrivia)
}

return child
}

return CodeBlockItemListSyntax(newChildren)
}
}
30 changes: 30 additions & 0 deletions Sources/PeripheryKit/CodeRemoval/EmptyFileVisitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation
import Foundation
import SwiftParser
import SwiftSyntax
import SystemPackage

final class EmptyFileVisitor: SyntaxVisitor, TriviaSplitting {
private var isEmpty = false

init() {
super.init(viewMode: .sourceAccurate)
}

func perform(syntax: SourceFileSyntax) -> Bool {
walk(syntax)
return isEmpty
}

override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind {
if node.statements.count == 0 {
isEmpty = true
} else {
isEmpty = node.statements.allSatisfy {
$0.item.is(ImportDeclSyntax.self) || $0.item.is(MissingDeclSyntax.self)
}
}

return .skipChildren
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import Foundation
import SwiftParser
import SwiftSyntax
import SystemPackage

final class PublicAccessibilitySyntaxRemover: SyntaxRewriter, SyntaxRemover {
private let resultLocation: SourceLocation
private let locationBuilder: SourceLocationBuilder

init(resultLocation: SourceLocation, replacements: [String], locationBuilder: SourceLocationBuilder) {
self.resultLocation = resultLocation
self.locationBuilder = locationBuilder
}

func perform(syntax: SourceFileSyntax) -> SourceFileSyntax {
visit(syntax)
}

override func visit(_ node: ClassDeclSyntax) -> DeclSyntax {
let newNode = removePublicModifier(
from: node,
at: node.name.positionAfterSkippingLeadingTrivia,
triviaRecipient: \.classKeyword
)
return super.visit(newNode)
}

override func visit(_ node: StructDeclSyntax) -> DeclSyntax {
let newNode = removePublicModifier(
from: node,
at: node.name.positionAfterSkippingLeadingTrivia,
triviaRecipient: \.structKeyword
)
return super.visit(newNode)
}

override func visit(_ node: EnumDeclSyntax) -> DeclSyntax {
let newNode = removePublicModifier(
from: node,
at: node.name.positionAfterSkippingLeadingTrivia,
triviaRecipient: \.enumKeyword
)
return super.visit(newNode)
}

override func visit(_ node: ExtensionDeclSyntax) -> DeclSyntax {
let newNode = removePublicModifier(
from: node,
at: node.extendedType.positionAfterSkippingLeadingTrivia,
triviaRecipient: \.extensionKeyword
)
return super.visit(newNode)
}

override func visit(_ node: ProtocolDeclSyntax) -> DeclSyntax {
let newNode = removePublicModifier(
from: node,
at: node.name.positionAfterSkippingLeadingTrivia,
triviaRecipient: \.protocolKeyword
)
return super.visit(newNode)
}

override func visit(_ node: InitializerDeclSyntax) -> DeclSyntax {
let newNode = removePublicModifier(
from: node,
at: node.initKeyword.positionAfterSkippingLeadingTrivia,
triviaRecipient: \.initKeyword
)
return super.visit(newNode)
}

override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax {
let newNode = removePublicModifier(
from: node,
at: node.name.positionAfterSkippingLeadingTrivia,
triviaRecipient: \.funcKeyword
)
return super.visit(newNode)
}

override func visit(_ node: SubscriptDeclSyntax) -> DeclSyntax {
let newNode = removePublicModifier(
from: node,
at: node.subscriptKeyword.positionAfterSkippingLeadingTrivia,
triviaRecipient: \.subscriptKeyword
)
return super.visit(newNode)
}

override func visit(_ node: TypeAliasDeclSyntax) -> DeclSyntax {
let newNode = removePublicModifier(
from: node,
at: node.name.positionAfterSkippingLeadingTrivia,
triviaRecipient: \.typealiasKeyword
)
return super.visit(newNode)
}

override func visit(_ node: VariableDeclSyntax) -> DeclSyntax {
let newNode = removePublicModifier(
from: node,
at: node.bindings.positionAfterSkippingLeadingTrivia,
triviaRecipient: \.bindingSpecifier
)
return super.visit(newNode)
}

override func visit(_ node: ActorDeclSyntax) -> DeclSyntax {
let newNode = removePublicModifier(
from: node,
at: node.name.positionAfterSkippingLeadingTrivia,
triviaRecipient: \.actorKeyword
)
return super.visit(newNode)
}

override func visit(_ node: AssociatedTypeDeclSyntax) -> DeclSyntax {
let newNode = removePublicModifier(
from: node,
at: node.name.positionAfterSkippingLeadingTrivia,
triviaRecipient: \.associatedtypeKeyword
)
return super.visit(newNode)
}

override func visit(_ node: PrecedenceGroupDeclSyntax) -> DeclSyntax {
let newNode = removePublicModifier(
from: node,
at: node.name.positionAfterSkippingLeadingTrivia,
triviaRecipient: \.precedencegroupKeyword
)
return super.visit(newNode)
}

// MARK: - Private

private func removePublicModifier<T: PublicModifiedDecl, Output: SyntaxProtocol>(
from node: T,
at position: AbsolutePosition,
triviaRecipient: WritableKeyPath<T, Output>
) -> T {
var removedLeadingTrivia = Trivia(pieces: [])
var didRemove = false

var newModifiers = node.modifiers.filter { modifier in
if locationBuilder.location(at: position) == resultLocation,
modifier.name.text == "public" {
didRemove = true
removedLeadingTrivia = modifier.leadingTrivia
return false
}

return true
}

var newNode = node

if didRemove {
if newModifiers.count == 0 {
let triviaRecipientNode = node[keyPath: triviaRecipient]
let newTriviaRecipientNode = triviaRecipientNode
.with(\.leadingTrivia, removedLeadingTrivia + newModifiers.leadingTrivia)
newNode = newNode.with(triviaRecipient, newTriviaRecipientNode)
} else {
newModifiers = newModifiers
.with(\.leadingTrivia, removedLeadingTrivia + newModifiers.leadingTrivia)
}

return newNode.with(\.modifiers, newModifiers)
} else {
return node
}
}
}

protocol PublicModifiedDecl: SyntaxProtocol {
var modifiers: DeclModifierListSyntax { get set }
}

extension ClassDeclSyntax: PublicModifiedDecl {}
extension StructDeclSyntax: PublicModifiedDecl {}
extension EnumDeclSyntax: PublicModifiedDecl {}
extension EnumCaseDeclSyntax: PublicModifiedDecl {}
extension ExtensionDeclSyntax: PublicModifiedDecl {}
extension ProtocolDeclSyntax: PublicModifiedDecl {}
extension InitializerDeclSyntax: PublicModifiedDecl {}
extension FunctionDeclSyntax: PublicModifiedDecl {}
extension SubscriptDeclSyntax: PublicModifiedDecl {}
extension TypeAliasDeclSyntax: PublicModifiedDecl {}
extension VariableDeclSyntax: PublicModifiedDecl {}
extension ActorDeclSyntax: PublicModifiedDecl {}
extension AssociatedTypeDeclSyntax: PublicModifiedDecl {}
extension PrecedenceGroupDeclSyntax: PublicModifiedDecl {}
Loading

0 comments on commit 1dc4bc4

Please sign in to comment.