legacy/swift/Sources/PiranhaKit/CleanupStaleFlags/StaleFlagCleaner.swift (667 lines of code) (raw):
/**
* Copyright (c) 2019 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
import SwiftSyntax
class XPFlagCleaner: SyntaxRewriter {
// specifies the value returned by evaluating the expression
private enum Value {
case isTrue
case isFalse
case isBot
}
// specifies the various operators: for now, simply binary
private enum Operator {
case and
case or
case nilcoalesc
case equality
case unknown
}
private let config: PiranhaConfig
private let SELFDOT: String = "self."
private let UNKNOWN: String = "unknown"
private var valueMap = [String: Value]()
private var deepCleanMap = [String: Value]()
private var fieldMap = [String: Bool]()
private var previousTrivia: Trivia = []
private var caseIndex: Int?
private var scopes = [Scope]()
private let flagName: String
private let groupName: String
private let isTreated: Bool
private var isDeepCleanPass: Bool
private var shouldDeepClean: Bool
init(with config: PiranhaConfig,
flag flagName: String,
behavior isTreated: Bool,
group groupName: String) {
self.config = config
self.flagName = flagName
self.isTreated = isTreated
self.groupName = groupName
isDeepCleanPass = false
shouldDeepClean = false
}
// checks for the binary operator to handle and/or
private func findOperator(from node: BinaryOperatorExprSyntax?) -> Operator {
guard let node = node else {
return Operator.unknown
}
let opTokenKind = node.operatorToken.tokenKind
if opTokenKind == TokenKind.spacedBinaryOperator("&&") {
return Operator.and
} else if opTokenKind == TokenKind.spacedBinaryOperator("||") {
return Operator.or
} else if opTokenKind == TokenKind.spacedBinaryOperator("??") {
return Operator.nilcoalesc
} else if opTokenKind == TokenKind.spacedBinaryOperator("==") {
return Operator.equality
}
return Operator.unknown
}
// given an argument list, get the argument at a specified index
private func argument(arglist args: TupleExprElementListSyntax,
_ index: Int) -> TupleExprElementSyntax? {
for (loopindex, argument) in args.enumerated() {
if loopindex == index {
return argument
}
}
return nil
}
// Helper function to get the type of a node. Used for debugging
private func type(of node: Syntax) -> Any.Type {
Mirror(reflecting: node).subjectType
}
// Returns the string representation of the node under consideration by concatenating all the tokens
// helps avoid trivia that is added, if any
private func string(of node: Syntax) -> String {
let tokenTexts = node.tokens.map { token in token.text }
return tokenTexts.joined()
}
// matches an argument name with the argument at a specific index to the function call.
private func match(in node: FunctionCallExprSyntax,
name: String, at index: Int) -> Bool {
if node.argumentList.count > 0,
let argument = argument(arglist: node.argumentList, index) {
if let expr = MemberAccessExprSyntax.init(Syntax(argument.expression)) {
if expr.name.description == name {
return true
}
if expr.name.description == "asString",
expr.dot.previousToken?.description == name {
return true
}
}
if let expr = ExprSyntax.init(Syntax(argument.expression)),
labelReplacementAllowed(forIdentifier: expr.description) {
return true
}
if let expr = StringLiteralExprSyntax.init(Syntax(argument.expression)) {
if name == expr.description.replacingOccurrences(of: "\"", with: "") {
return true
}
}
}
return false
}
override func visitPre(_ node: Syntax) {
if let _ = node.asProtocol(DeclSyntaxProtocol.self) as? FunctionDeclSyntax {
scopes.append(Scope())
}
super.visitPre(node)
}
override func visitPost(_ node: Syntax) {
if let _ = node.asProtocol(DeclSyntaxProtocol.self) as? FunctionDeclSyntax {
scopes.removeLast()
}
super.visitPost(node)
}
// gets the type of the flag API
private func flagApiType(of node: FunctionCallExprSyntax) -> Method.FlagType? {
for method in config.methods {
if node.calledExpression.description.hasSuffix(method.methodName),
match(in: node, name: flagName, at: method.flagIndex) {
var foundMatch = true
// if there is a groupIndex and it doesn't match with groupName,
if method.groupIndex != nil, !match(in: node, name: groupName, at: method.groupIndex!) {
foundMatch = false
}
if foundMatch {
return method.flagType
}
}
}
return nil
}
// appends the input statements to the result node and returns the updated result node along with information on whether the last node is a return
private func append(node: CodeBlockItemListSyntax, with stmts: CodeBlockItemListSyntax) -> (CodeBlockItemListSyntax, Bool) {
var result = node
var lastNodeIsReturn = false
for statement in stmts {
result = result.appending(statement)
if let _ = ReturnStmtSyntax.init(statement.item) {
lastNodeIsReturn = true
break
}
}
return (result, lastNodeIsReturn)
}
// get the prefix from node upto the specified index
// TODO: How to use node.prefix(index) and convert it into ExprListSyntax?
private func prefix(from node: ExprListSyntax, upto index: Int) -> ExprListSyntax {
var result = SyntaxFactory.makeBlankExprList()
for (loopindex, expr) in node.enumerated() {
if loopindex < index {
result = result.appending(expr)
} else {
break
}
}
return result
}
// get the suffix from node starting at index
// TODO: How to use node.suffix(...) and convert it into ExprListSyntax?
private func suffix(from node: ExprListSyntax, after index: Int) -> ExprListSyntax {
var result = SyntaxFactory.makeBlankExprList()
for (loopindex, expr) in node.enumerated() {
if loopindex > index {
result = result.appending(expr)
}
}
return result
}
// Returns an element from exprlist at the given index
private func element(from exprlist: ExprListSyntax, at index: Int) -> ExprSyntax? {
// TODO: Value of type 'ExprListSyntax' has no subscripts
// return (index < exprlist.count) ? exprlist[index] : nil
for (loopindex, expr) in exprlist.enumerated() {
if loopindex == index {
return expr
}
}
return nil
}
// helper for caching the value into the valueMap.
private func cache(expression node: Syntax, with value: Value) -> Value {
valueMap[node.description] = value
return value
}
// evaluates different expressions and is the core algorithm for modifications
private func evaluate(expression node: Syntax) -> Value {
// if it is a deep clean pass and the relevant node (e.g., field access) is present in the deepCleanMap,
// return the associated value.
if isDeepCleanPass, let value = deepCleanMap[string(of: node)] {
return value
}
if let value = scopeResolvedValue(for: node) {
return value
}
// if the expression is previously evaluated in the current pass, return the value
if let value = valueMap[node.description] {
return value
}
if let booleanNode = BooleanLiteralExprSyntax.init(node) {
// handles "true" or "false"
if booleanNode.booleanLiteral.tokenKind == TokenKind.trueKeyword {
return Value.isTrue
}
if case .identifier("true") = booleanNode.booleanLiteral.tokenKind {
return Value.isTrue
}
return Value.isFalse
}
else if let prefixOperatorNode = PrefixOperatorExprSyntax.init(node) {
// handles negation
if prefixOperatorNode.operatorToken?.tokenKind == TokenKind.prefixOperator("!") {
let value = evaluate(expression: Syntax(prefixOperatorNode.postfixExpression))
if value == Value.isTrue {
return cache(expression: Syntax(prefixOperatorNode), with: Value.isFalse)
} else if value == Value.isFalse {
return cache(expression: Syntax(prefixOperatorNode), with: Value.isTrue)
}
}
return cache(expression: Syntax(prefixOperatorNode), with: Value.isBot)
} else if let initializerNode = InitializerClauseSyntax.init(node) {
if let _ = BooleanLiteralExprSyntax.init(Syntax(initializerNode.value)) {
return Value.isBot
}
return evaluate(expression: Syntax(initializerNode.value))
} else if let conditionElementListNode = ConditionElementListSyntax.init(node) {
// handles condition element lists
for element in conditionElementListNode {
let value = evaluate(expression: element.condition)
// if there is only one element in the condition, just return its value
// if there are multiple elements and one of them is false, return that
if conditionElementListNode.count == 1 || value == Value.isFalse {
return cache(expression: Syntax(conditionElementListNode), with: value)
}
}
} else if let functionCallExprNode = FunctionCallExprSyntax.init(node) {
// handles flag API calls
var value = Value.isBot
let api = flagApiType(of: functionCallExprNode)
if api == .treated {
value = (isTreated ? Value.isTrue : Value.isFalse)
} else if api == .control {
value = (isTreated ? Value.isFalse : Value.isTrue)
}
return cache(expression: Syntax(functionCallExprNode), with: value)
} else if let sequenceExprNode = SequenceExprSyntax.init(node) {
// handles sequence expressions
// examples include:
// a && b || c && d
// a = b && c || d
// a ?? b, etc
return cache(expression: sequenceExprNode._syntaxNode, with: evaluate(sequence: sequenceExprNode))
} else if let tupleExprNode = TupleExprSyntax.init(node) {
// handle (a)
// TODO: Is there a better way of getting the first element from this collection?
if let firstChild = tupleExprNode.elementList.first(where: { $0.indexInParent == 0 }) {
return cache(expression: Syntax(tupleExprNode), with: evaluate(expression: Syntax(firstChild.expression)))
}
} else {
// for all other cases, cache bot val
return cache(expression: node, with: Value.isBot)
}
return Value.isBot
}
private func scopeResolvedValue(for node: Syntax) -> Value? {
guard let functionCallExprNode = FunctionCallExprSyntax.init(node) else {
return nil
}
let api = flagApiType(of: functionCallExprNode)
if api == .treated {
return (isTreated ? Value.isTrue : Value.isFalse)
} else if api == .control {
return (isTreated ? Value.isFalse : Value.isTrue)
}
return nil
}
// evaluate a given sequence
private func evaluate(sequence expr: SequenceExprSyntax) -> Value {
if expr.elements.count == 1,
let onlyElement = element(from: expr.elements, at: 0) {
return evaluate(expression: Syntax(onlyElement))
}
for (index, expression) in expr.elements.enumerated() {
if let _ = AssignmentExprSyntax.init(Syntax(expression)) {
return Value.isBot
}
if let _ = TernaryExprSyntax.init(Syntax(expression)) {
return Value.isBot
}
if let binaryExpr = BinaryOperatorExprSyntax.init(Syntax(expression)) {
let opKind = findOperator(from: binaryExpr)
if opKind == Operator.and ||
opKind == Operator.or ||
opKind == Operator.equality {
let lhs = prefix(from: expr.elements, upto: index)
let rhs = suffix(from: expr.elements, after: index)
let result = evaluate(lhs: lhs, rhs: rhs, kind: opKind)
if result != Value.isBot {
return result
}
} else if opKind == Operator.nilcoalesc {
return evaluate(expression: Syntax(element(from: expr.elements, at: 0)!))
}
}
}
return Value.isBot
}
// evaluate a binary expression, given lhs value, rhs value and the operation kind
private func evaluate(lhs: ExprListSyntax, rhs: ExprListSyntax, kind opKind: Operator) -> Value {
let lhsVal = evaluate(expression: Syntax(SyntaxFactory.makeSequenceExpr(elements: lhs)))
let rhsVal = evaluate(expression: Syntax(SyntaxFactory.makeSequenceExpr(elements: rhs)))
if opKind == Operator.or {
if lhsVal == Value.isTrue || rhsVal == Value.isTrue {
return Value.isTrue
}
if lhsVal == Value.isFalse, rhsVal == Value.isFalse {
return Value.isFalse
}
} else if opKind == Operator.and {
if lhsVal == Value.isTrue && rhsVal == Value.isTrue {
return Value.isTrue
}
if lhsVal == Value.isFalse || rhsVal == Value.isFalse {
return Value.isFalse
}
} else if opKind == Operator.equality {
if lhsVal != Value.isBot && rhsVal != Value.isBot {
return (lhsVal == rhsVal) ? Value.isTrue : Value.isFalse
}
}
return Value.isBot
}
// Evaluate the node of type ClosureExprSyntax
// In a return/if, if the node contains the flagName + ".asString"/".rawValue",
// return true.
// this pattern may be custom usage of flags and may not be generically applicable.
// If so, can be put behind an option.
private func evaluate(node: ClosureExprSyntax) -> Bool {
let key = flagName + ".asString"
let value = flagName + ".rawValue"
for statement in node.statements {
if let returnNode = ReturnStmtSyntax.init(statement.item) {
if returnNode.description.contains(key) || returnNode.description.contains(value) {
return true
}
} else if let ifNode = IfStmtSyntax.init(statement.item) {
if ifNode.description.contains(key) || ifNode.description.contains(value) {
return true
}
}
}
return false
}
// simplify an exprlist and return the updated exprlist along with its evaluation
private func simplify(exprlist: ExprListSyntax) -> (ExprListSyntax, Value) {
let sequence = SyntaxFactory.makeSequenceExpr(elements: exprlist)
// evaluate the sequence. if it is a bot, explore possibility of further simplification within itself
// e.g., a && true is a bot, but can be reduced to a
let value = evaluate(expression: Syntax(sequence))
if value == Value.isBot,
let updated = SequenceExprSyntax.init(Syntax(simplify(node: sequence))) {
return (updated.elements, value)
}
// unable to do any further simplification
return (exprlist, value)
}
// Used for simplifying binary expressions
// simplify the expression list of elements containing operator of kind opKind at
// index. always returns a non-empty sequence
private func simplify(exprlist elements: ExprListSyntax, containing opKind: Operator,
at index: Int) -> ExprListSyntax {
// get the lhs and rhs lists
var lhs = prefix(from: elements, upto: index)
var rhs = suffix(from: elements, after: index)
var lhsVal: Value
var rhsVal: Value
(lhs, lhsVal) = simplify(exprlist: lhs)
(rhs, rhsVal) = simplify(exprlist: rhs)
// if there are concrete values, one side of the list could be discarded
if (opKind == Operator.and && lhsVal == Value.isTrue) ||
(opKind == Operator.or && lhsVal == Value.isFalse) {
return rhs
} else if (opKind == Operator.and && rhsVal == Value.isTrue) ||
(opKind == Operator.or && rhsVal == Value.isFalse) {
return lhs
}
// if neither side could be discarded, append the reduced lists along with the operator and return
lhs = lhs.appending(element(from: elements, at: index)!)
for v in rhs {
lhs = lhs.appending(v)
}
return lhs
}
// Used for simplifying ternary expressions
// Simplify the exprlist with ternary node at index.
// If the ternary node conditional expression is true, pick the first choice.
// Otherwise, the second choice. If the evaluation returns a false, no simplification
// is performed.
private func simplify(exprlist: ExprListSyntax, ternary node: TernaryExprSyntax, at index: Int) -> ExprListSyntax {
var result = SyntaxFactory.makeBlankExprList()
// split the expression at index
// TODO: Explore whether there a better way for doing the split in Swift?
for (loopindex, expr) in exprlist.enumerated() {
if loopindex < index {
result = result.appending(expr)
} else {
break
}
}
let v = evaluate(expression: Syntax(node.conditionExpression))
switch v {
case Value.isTrue:
result = result.appending(node.firstChoice)
case Value.isFalse:
result = result.appending(node.secondChoice)
case Value.isBot:
result = exprlist
}
return result
}
private func updateFieldMap(with key: String) {
fieldMap[key] = true
fieldMap[SELFDOT + key] = true
}
private func updateDeepCleanMap(with key: String, value val: Value) {
shouldDeepClean = true
deepCleanMap[key] = val
deepCleanMap[SELFDOT + key] = val
}
// Used for simplifying assignments
// This function needs to be refactored, especially the DeepCleanPass parts
private func simplify(assignment exprlist: ExprListSyntax) -> ExprListSyntax {
let lhs = element(from: exprlist, at: 0)!
if isDeepCleanPass {
var key: String?
if let memberAccessExpr = MemberAccessExprSyntax.init(Syntax(lhs)) {
key = string(of: Syntax(memberAccessExpr))
} else if let identifierExpr = IdentifierExprSyntax.init(Syntax(lhs)) {
key = SELFDOT + string(of: Syntax(identifierExpr))
}
if deepCleanMap.keys.contains(key ?? UNKNOWN) {
// FIXME: currently, refactors even new definitions in the deep clean pass if the
// variable was simplified in the previous pass.
return SyntaxFactory.makeBlankExprList()
}
}
var rhs = SyntaxFactory.makeBlankExprList()
for (loopindex, expr) in exprlist.enumerated() {
// the rhs of the assignment containing the closureexpression.
if loopindex > 1 {
if let closureExpr = ClosureExprSyntax.init(Syntax(expr)),
evaluate(node: closureExpr) {
// if the closure expression evaluates to true,
// (i.e., contains the flag name .asString/.rawValue)
// delete the entire assignment and return empty expression list
return SyntaxFactory.makeBlankExprList()
}
rhs = rhs.appending(expr)
}
}
// handles the scenarios where the RHS is one element and has an API that is not
// evaluated but is removed (e.g., flag testing API)
// The resultant refactoring will delete the entire assignment
if rhs.count == 1 {
if let node = element(from: exprlist, at: 2),
let callExpr = FunctionCallExprSyntax.init(Syntax(node)) {
if flagApiType(of: callExpr) == .testing {
return SyntaxFactory.makeBlankExprList()
} else {
let rhsValue = evaluate(expression: Syntax(node))
// if it is the initial pass, then update the deepCleanMap because there is
// an assignment that evaluated to true or false, and all accesses of that
// lhs should also be evaluated appropriately for further cleanup
if !isDeepCleanPass, rhsValue != Value.isBot {
if let memberAccessExpr = MemberAccessExprSyntax.init(Syntax(lhs)) {
shouldDeepClean = true
// TODO: Comment on why each key is being put in the deepCleanMap
var key = string(of: Syntax(memberAccessExpr))
deepCleanMap[key] = rhsValue // put in qualified fieldName
key = string(of: Syntax(memberAccessExpr.name))
deepCleanMap[key] = rhsValue // put in fieldName
} else if let identifierExpr = IdentifierExprSyntax.init(Syntax(lhs)) {
let key = string(of: Syntax(identifierExpr))
updateDeepCleanMap(with: key, value: rhsValue)
}
}
}
}
}
if let assignment = element(from: exprlist, at: 1) {
let rhsSequence = SyntaxFactory.makeSequenceExpr(elements: rhs)
return SyntaxFactory.makeExprList([lhs, assignment, simplify(node: rhsSequence)])
}
// unable to simplify. so, return as is
return exprlist
}
// helper to handle seq expression
private func simplify(node: SequenceExprSyntax) -> ExprSyntax {
let value = evaluate(expression: Syntax(node))
switch value {
case Value.isTrue:
let booleanLiteralExpr = SyntaxFactory.makeBooleanLiteralExpr(booleanLiteral: SyntaxFactory.makeToken(.identifier("true"),
presence: .present))
return ExprSyntax.init(booleanLiteralExpr)
case Value.isFalse:
let booleanLiteralExpr = SyntaxFactory.makeBooleanLiteralExpr(booleanLiteral: SyntaxFactory.makeToken(.identifier("false"),
presence: .present))
return ExprSyntax.init(booleanLiteralExpr)
case Value.isBot:
var result = SyntaxFactory.makeBlankExprList()
for (index, expr) in node.elements.enumerated() {
if InitializerClauseSyntax.init(Syntax(expr)) != nil ||
AssignmentExprSyntax.init(Syntax(expr)) != nil {
result = simplify(assignment: node.elements)
if result.count == 0 {
return ExprSyntax.init(SyntaxFactory.makeBlankSequenceExpr())
}
} else if let binaryExpr = BinaryOperatorExprSyntax.init(Syntax(expr)) {
let opKind = findOperator(from: binaryExpr)
result = simplify(exprlist: node.elements, containing: opKind, at: index)
return super.visit(node.withElements(result))
} else if let ternaryExpr = TernaryExprSyntax.init(Syntax(expr)) {
result = simplify(exprlist: node.elements, ternary: ternaryExpr, at: index)
return super.visit(node.withElements(result))
}
if result.count > 0 {
return super.visit(node.withElements(result))
}
}
}
return super.visit(node)
}
override func visit(_ node: ConditionElementListSyntax) -> Syntax {
// if it is just one node, just let the visitor for that node perform the processing
if node.count == 1 {
return super.visit(node)
}
var result = SyntaxFactory.makeBlankConditionElementList()
for expr in node {
let value = evaluate(expression: expr.condition)
if value != Value.isTrue {
result = result.appending(expr)
}
}
result.removeLastTrailingCommaIfNeeded()
return super.visit(result)
}
override func visit(_ node: SequenceExprSyntax) -> ExprSyntax {
// handling some custom code that should not be refactored
if node.description.hasSuffix("recordMode = false || platformUIChange") {
return super.visit(node)
}
return simplify(node: node)
}
override func visit(_ node: ArrayElementListSyntax) -> Syntax {
var newNode = SyntaxFactory.makeBlankArrayElementList()
for expr in node {
if !expr.description.contains(flagName) {
newNode = newNode.appending(expr)
}
}
return super.visit(newNode)
}
override func visit(_ node: EnumDeclSyntax) -> DeclSyntax {
if node.identifier.description == flagName {
return DeclSyntax.init(SyntaxFactory.makeBlankEnumDecl())
}
return super.visit(node)
}
/*
---------------------------
case random_flag // comment1
// comment2
case stale_flag // comment3
// comment 4
case another_flag
---------------------------
should translate to
----------------------------
case random_flag // comment1
//comment4
case another_flag
----------------------------
*/
override func visit(_ node: EnumCaseDeclSyntax) -> DeclSyntax {
guard node.elements.count > 0 else {
return super.visit(node)
}
var indexInParent: Int?
if let nodeparent = node.parent {
indexInParent = nodeparent.indexInParent
}
// TODO: Is there a better way of getting the first element from this sequence?
if let firstElement = node.elements.first(where: { $0.indexInParent == 0 }),
flagName == string(of: Syntax(firstElement)),
let indexInParent = indexInParent {
caseIndex = indexInParent + 1
if let leadingTrivia = node.leadingTrivia {
previousTrivia = []
for i in leadingTrivia {
previousTrivia = previousTrivia.appending(i) // saves comment1
if case .newlines = i {
break
}
}
}
/* this gets rid of the leading trivia for the current decl
e.g., some comments and case flagname will be handled
From the above example, gets rid of
-----------------
// comment1
// comment2
case stale_flag
*/
return DeclSyntax.init(SyntaxFactory.makeBlankEnumCaseDecl())
}
// this will handle the leading trivia for the next token.
// e.g., case stale_flagname // this flag is stale
// case random_flag //
// the string "// this flag is stale" is attached to the node "case random_flag"
// so, we need to perform the following operations to cleanup the code
defer {
caseIndex = nil
previousTrivia = []
}
if caseIndex == indexInParent,
let leadingTrivia = node.leadingTrivia {
var updatedTrivia: Trivia = []
// update any previous trivia from the deleted node
// adds "// comment1" to the trivia
for trivia in previousTrivia {
updatedTrivia = updatedTrivia.appending(trivia)
}
// remove the first trivia piece and leave the rest
// drops " //comment3" and adds "//comment4"
for trivia in leadingTrivia.dropFirst() {
updatedTrivia = updatedTrivia.appending(trivia)
}
/* update the keyword
the newtrivia will be
// comment1
//comment4
*/
let newCaseKeyword = node.caseKeyword.withLeadingTrivia(updatedTrivia)
// visit with the updated keyword
return super.visit(node.withCaseKeyword(newCaseKeyword))
}
return super.visit(node)
}
override func visit(_ node: MemberDeclBlockSyntax) -> Syntax {
// update the list of possible fields that is used to filter the deepCleanMap
if !isDeepCleanPass {
for member in node.members {
if let variableDecl = VariableDeclSyntax.init(Syntax(member.decl)) {
for binding in variableDecl.bindings {
updateFieldMap(with: string(of: Syntax(binding.pattern)))
}
}
}
}
return super.visit(node)
}
override func visit(_ node: VariableDeclSyntax) -> DeclSyntax {
for binding in node.bindings {
if isDeepCleanPass, deepCleanMap.keys.contains(SELFDOT + string(of: Syntax(binding.pattern))) {
return visit(SyntaxFactory.makeBlankVariableDecl())
}
if let rhs = binding.initializer {
updateScope(withIdentifier: binding.pattern.description.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines),
havingValue: false)
let rhsValue = evaluate(expression: Syntax(rhs))
if rhsValue != Value.isBot {
let key = string(of: Syntax(binding.pattern))
updateFieldMap(with: key)
updateDeepCleanMap(with: key, value: rhsValue)
return visit(SyntaxFactory.makeBlankVariableDecl())
}
if let value = FunctionCallExprSyntax.init(Syntax(rhs.value)) {
if (flagApiType(of: value) == .testing) || (evaluate(expression: Syntax(rhs.value)) != Value.isBot) {
return visit(SyntaxFactory.makeBlankVariableDecl())
}
}
if let value = MemberAccessExprSyntax.init(Syntax(rhs.value)),
value.description.hasSuffix(flagName) {
updateScope(withIdentifier: binding.pattern.description.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines),
havingValue: true)
return visit(SyntaxFactory.makeBlankVariableDecl())
}
}
}
return super.visit(node)
}
var replacements: [BooleanLiteralExprSyntax] = []
override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
let value = evaluate(expression: Syntax(node))
switch value {
case Value.isTrue:
let booleanLiteralExpr = SyntaxFactory.makeBooleanLiteralExpr(booleanLiteral: SyntaxFactory.makeToken(.identifier("true"),
presence: .present))
return visit(booleanLiteralExpr)
case Value.isFalse:
let booleanLiteralExpr = SyntaxFactory.makeBooleanLiteralExpr(booleanLiteral: SyntaxFactory.makeToken(.identifier("false"),
presence: .present))
return visit(booleanLiteralExpr)
case Value.isBot:
return super.visit(node)
}
}
override func visit(_ node: CodeBlockItemListSyntax) -> Syntax {
var newBody = SyntaxFactory.makeBlankCodeBlockItemList()
var lastNodeIsReturn = false
for statement in node {
if let ifNode = IfStmtSyntax.init(statement.item) {
let value = evaluate(expression: Syntax(ifNode.conditions))
switch value {
case Value.isBot: newBody = newBody.appending(statement)
case Value.isTrue:
(newBody, lastNodeIsReturn) = append(node: newBody, with: ifNode.body.statements)
case Value.isFalse:
if let elseBodyNode = ifNode.elseBody,
let elseBody = CodeBlockSyntax.init(Syntax(elseBodyNode)) {
(newBody, lastNodeIsReturn) = append(node: newBody, with: elseBody.statements)
}
}
if lastNodeIsReturn {
return super.visit(newBody)
}
} else if let guardNode = GuardStmtSyntax.init(statement.item) {
let value = evaluate(expression: Syntax(guardNode.conditions))
switch value {
case Value.isBot: newBody = newBody.appending(statement)
case Value.isTrue:
break
case Value.isFalse:
(newBody, lastNodeIsReturn) = append(node: newBody, with: guardNode.body.statements)
if lastNodeIsReturn {
return super.visit(newBody)
}
}
} else if let callNode = FunctionCallExprSyntax.init(statement.item),
flagApiType(of: callNode) == .testing {
// do nothing, as the test API needs to be discarded
} else {
newBody = newBody.appending(statement)
}
}
return super.visit(newBody)
}
func setNextPass() {
// clear up all cached valuemaps.
valueMap.removeAll()
// if it is not a field, remove it from deepcleaning
// update the valuemap also.
for (key, value) in deepCleanMap {
if !fieldMap.keys.contains(key) {
deepCleanMap.removeValue(forKey: key)
} else {
valueMap[key] = value
}
}
if shouldDeepClean {
isDeepCleanPass = true
}
}
func deepClean() -> Bool {
return shouldDeepClean
}
}
private extension XPFlagCleaner {
final class Scope {
// True signifies that the replacement can happen and false signifies that replacement shouldn't happen.
var labelVariableMap: [String: Bool] = [:]
}
private func updateScope(withIdentifier identifier: String,
havingValue value: Bool) {
guard let scope = scopes.last else { return }
scope.labelVariableMap[identifier] = value
}
private func labelReplacementAllowed(forIdentifier identifier: String) -> Bool {
for scope in scopes.reversed() {
/// If the closest scope label is found that allows replacement, return true.
if scope.labelVariableMap[identifier] == true {
return true
}
/// If the closest scope label is found that doesn't allow replacement, return false.
if scope.labelVariableMap[identifier] == false {
return false
}
}
return false
}
}