<commit_before>/*
 Copyright 2016-present The Material Motion Authors. All Rights Reserved.

 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

public typealias JSONObject = Any

typealias SourceKittenNode = [String: Any]
typealias APINode = [String: Any]
typealias ApiNameNodeMap = [String: APINode]

/** A type of API change. */
public enum ApiChange {
  case Addition(apiType: String, name: String)
  case Deletion(apiType: String, name: String)
  case Modification(apiType: String, name: String, modificationType: String, from: String, to: String)
}

/** Generates an API diff report from two SourceKitten JSON outputs. */
public func diffreport(oldApi: JSONObject, newApi: JSONObject) throws -> [String: [ApiChange]] {
  let oldApiNameNodeMap = extractAPINodeMap(from: oldApi as! [SourceKittenNode])
  let newApiNameNodeMap = extractAPINodeMap(from: newApi as! [SourceKittenNode])

  let oldApiNames = Set(oldApiNameNodeMap.keys)
  let newApiNames = Set(newApiNameNodeMap.keys)

  let addedApiNames = newApiNames.subtracting(oldApiNames)
  let deletedApiNames = oldApiNames.subtracting(newApiNames)
  let persistedApiNames = oldApiNames.intersection(newApiNames)

  var changes: [String: [ApiChange]] = [:]

  // Additions

  for usr in (addedApiNames.map { usr in newApiNameNodeMap[usr]! }.sorted(by: apiNodeIsOrderedBefore)) {
    let apiType = prettyString(forKind: usr["key.kind"] as! String)
    let name = prettyName(forApi: usr, apis: newApiNameNodeMap)
    let root = rootName(forApi: usr, apis: newApiNameNodeMap)
    changes[root, withDefault: []].append(.Addition(apiType: apiType, name: name))
  }

  // Deletions

  for usr in (deletedApiNames.map { usr in oldApiNameNodeMap[usr]! }.sorted(by: apiNodeIsOrderedBefore)) {
    let apiType = prettyString(forKind: usr["key.kind"] as! String)
    let name = prettyName(forApi: usr, apis: oldApiNameNodeMap)
    let root = rootName(forApi: usr, apis: oldApiNameNodeMap)
    changes[root, withDefault: []].append(.Deletion(apiType: apiType, name: name))
  }

  // Modifications

  let ignoredKeys = Set(arrayLiteral: "key.doc.line", "key.parsed_scope.end", "key.parsed_scope.start", "key.doc.column", "key.doc.comment")

  for usr in persistedApiNames {
    let oldApi = oldApiNameNodeMap[usr]!
    let newApi = newApiNameNodeMap[usr]!
    let root = rootName(forApi: newApi, apis: newApiNameNodeMap)
    let allKeys = Set(oldApi.keys).union(Set(newApi.keys))

    for key in allKeys {
      if ignoredKeys.contains(key) {
        continue
      }
      if let oldValue = oldApi[key] as? String, let newValue = newApi[key] as? String, oldValue != newValue {
        let apiType = prettyString(forKind: newApi["key.kind"] as! String)
        let name = prettyName(forApi: newApi, apis: newApiNameNodeMap)
        let modificationType = prettyString(forModificationKind: key)
        changes[root, withDefault: []].append(.Modification(apiType: apiType,
                                                            name: name,
                                                            modificationType: modificationType,
                                                            from: oldValue,
                                                            to: newValue))
      }
    }
  }

  return changes
}

extension ApiChange {
  public func toMarkdown() -> String {
    switch self {
    case .Addition(let apiType, let name):
      return "*new* \(apiType): \(name)"
    case .Deletion(let apiType, let name):
      return "*removed* \(apiType): \(name)"
    case .Modification(let apiType, let name, let modificationType, let from, let to):
      return [
        "*modified* \(apiType): \(name)",
        "",
        "| Type of change: | \(modificationType) |",
        "|---|---|",
        "| From: | `\(from.replacingOccurrences(of: "\n", with: " "))` |",
        "| To: | `\(to.replacingOccurrences(of: "\n", with: " "))` |"
      ].joined(separator: "\n")
    }
  }
}

extension ApiChange: Equatable {}

public func == (left: ApiChange, right: ApiChange) -> Bool {
  switch (left, right) {
  case (let .Addition(apiType, name), let .Addition(apiType2, name2)):
    return apiType == apiType2 && name == name2
  case (let .Deletion(apiType, name), let .Deletion(apiType2, name2)):
    return apiType == apiType2 && name == name2
  case (let .Modification(apiType, name, modificationType, from, to),
        let .Modification(apiType2, name2, modificationType2, from2, to2)):
    return apiType == apiType2 && name == name2 && modificationType == modificationType2 && from == from2 && to == to2
  default:
    return false
  }
}

/**
 get-with-default API for Dictionary

 Example usage: dict[key, withDefault: []]
 */
extension Dictionary {
  subscript(key: Key, withDefault value: @autoclosure () -> Value) -> Value {
    mutating get {
      if self[key] == nil {
        self[key] = value()
      }
      return self[key]!
    }
    set {
      self[key] = newValue
    }
  }
}

/**
 Sorting function for APINode instances.

 Sorts by filename.

 Example usage: sorted(by: apiNodeIsOrderedBefore)
 */
func apiNodeIsOrderedBefore(prev: APINode, next: APINode) -> Bool {
  if let prevFile = prev["key.doc.file"] as? String, let nextFile = next["key.doc.file"] as? String {
    return prevFile < nextFile
  }
  return false
}

/** Union two dictionaries, preferring existing values if they possess a parent.usr key. */
func += (left: inout ApiNameNodeMap, right: ApiNameNodeMap) {
  for (k, v) in right {
    if left[k] == nil {
      left.updateValue(v, forKey: k)
    } else if let object = left[k], object["parent.usr"] == nil {
      left.updateValue(v, forKey: k)
    }
  }
}

func prettyString(forKind kind: String) -> String {
  if let pretty = [
    // Objective-C
    "sourcekitten.source.lang.objc.decl.protocol": "protocol",
    "sourcekitten.source.lang.objc.decl.typedef": "typedef",
    "sourcekitten.source.lang.objc.decl.method.instance": "method",
    "sourcekitten.source.lang.objc.decl.property": "property",
    "sourcekitten.source.lang.objc.decl.class": "class",

    // Swift
    "source.lang.swift.decl.function.method.instance": "method",
    "source.lang.swift.decl.var.instance": "var",
    "source.lang.swift.decl.class": "class",
    "source.lang.swift.decl.var.static": "static var",
    "source.lang.swift.decl.enum": "enum"
    ][kind] {
    return pretty
  }
  return kind
}

func prettyString(forModificationKind kind: String) -> String {
  switch kind {
  case "key.swift_declaration": return "swift declaration"
  case "key.parsed_declaration": return "declaration"
  default: return kind
  }
}

/** Walk the APINode to the root node. */<commit_msg>Add more Objective-C constant name mappings.<commit_after>/*
 Copyright 2016-present The Material Motion Authors. All Rights Reserved.

 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

public typealias JSONObject = Any

typealias SourceKittenNode = [String: Any]
typealias APINode = [String: Any]
typealias ApiNameNodeMap = [String: APINode]

/** A type of API change. */
public enum ApiChange {
  case Addition(apiType: String, name: String)
  case Deletion(apiType: String, name: String)
  case Modification(apiType: String, name: String, modificationType: String, from: String, to: String)
}

/** Generates an API diff report from two SourceKitten JSON outputs. */
public func diffreport(oldApi: JSONObject, newApi: JSONObject) throws -> [String: [ApiChange]] {
  let oldApiNameNodeMap = extractAPINodeMap(from: oldApi as! [SourceKittenNode])
  let newApiNameNodeMap = extractAPINodeMap(from: newApi as! [SourceKittenNode])

  let oldApiNames = Set(oldApiNameNodeMap.keys)
  let newApiNames = Set(newApiNameNodeMap.keys)

  let addedApiNames = newApiNames.subtracting(oldApiNames)
  let deletedApiNames = oldApiNames.subtracting(newApiNames)
  let persistedApiNames = oldApiNames.intersection(newApiNames)

  var changes: [String: [ApiChange]] = [:]

  // Additions

  for usr in (addedApiNames.map { usr in newApiNameNodeMap[usr]! }.sorted(by: apiNodeIsOrderedBefore)) {
    let apiType = prettyString(forKind: usr["key.kind"] as! String)
    let name = prettyName(forApi: usr, apis: newApiNameNodeMap)
    let root = rootName(forApi: usr, apis: newApiNameNodeMap)
    changes[root, withDefault: []].append(.Addition(apiType: apiType, name: name))
  }

  // Deletions

  for usr in (deletedApiNames.map { usr in oldApiNameNodeMap[usr]! }.sorted(by: apiNodeIsOrderedBefore)) {
    let apiType = prettyString(forKind: usr["key.kind"] as! String)
    let name = prettyName(forApi: usr, apis: oldApiNameNodeMap)
    let root = rootName(forApi: usr, apis: oldApiNameNodeMap)
    changes[root, withDefault: []].append(.Deletion(apiType: apiType, name: name))
  }

  // Modifications

  let ignoredKeys = Set(arrayLiteral: "key.doc.line", "key.parsed_scope.end", "key.parsed_scope.start", "key.doc.column", "key.doc.comment")

  for usr in persistedApiNames {
    let oldApi = oldApiNameNodeMap[usr]!
    let newApi = newApiNameNodeMap[usr]!
    let root = rootName(forApi: newApi, apis: newApiNameNodeMap)
    let allKeys = Set(oldApi.keys).union(Set(newApi.keys))

    for key in allKeys {
      if ignoredKeys.contains(key) {
        continue
      }
      if let oldValue = oldApi[key] as? String, let newValue = newApi[key] as? String, oldValue != newValue {
        let apiType = prettyString(forKind: newApi["key.kind"] as! String)
        let name = prettyName(forApi: newApi, apis: newApiNameNodeMap)
        let modificationType = prettyString(forModificationKind: key)
        changes[root, withDefault: []].append(.Modification(apiType: apiType,
                                                            name: name,
                                                            modificationType: modificationType,
                                                            from: oldValue,
                                                            to: newValue))
      }
    }
  }

  return changes
}

extension ApiChange {
  public func toMarkdown() -> String {
    switch self {
    case .Addition(let apiType, let name):
      return "*new* \(apiType): \(name)"
    case .Deletion(let apiType, let name):
      return "*removed* \(apiType): \(name)"
    case .Modification(let apiType, let name, let modificationType, let from, let to):
      return [
        "*modified* \(apiType): \(name)",
        "",
        "| Type of change: | \(modificationType) |",
        "|---|---|",
        "| From: | `\(from.replacingOccurrences(of: "\n", with: " "))` |",
        "| To: | `\(to.replacingOccurrences(of: "\n", with: " "))` |"
      ].joined(separator: "\n")
    }
  }
}

extension ApiChange: Equatable {}

public func == (left: ApiChange, right: ApiChange) -> Bool {
  switch (left, right) {
  case (let .Addition(apiType, name), let .Addition(apiType2, name2)):
    return apiType == apiType2 && name == name2
  case (let .Deletion(apiType, name), let .Deletion(apiType2, name2)):
    return apiType == apiType2 && name == name2
  case (let .Modification(apiType, name, modificationType, from, to),
        let .Modification(apiType2, name2, modificationType2, from2, to2)):
    return apiType == apiType2 && name == name2 && modificationType == modificationType2 && from == from2 && to == to2
  default:
    return false
  }
}

/**
 get-with-default API for Dictionary

 Example usage: dict[key, withDefault: []]
 */
extension Dictionary {
  subscript(key: Key, withDefault value: @autoclosure () -> Value) -> Value {
    mutating get {
      if self[key] == nil {
        self[key] = value()
      }
      return self[key]!
    }
    set {
      self[key] = newValue
    }
  }
}

/**
 Sorting function for APINode instances.

 Sorts by filename.

 Example usage: sorted(by: apiNodeIsOrderedBefore)
 */
func apiNodeIsOrderedBefore(prev: APINode, next: APINode) -> Bool {
  if let prevFile = prev["key.doc.file"] as? String, let nextFile = next["key.doc.file"] as? String {
    return prevFile < nextFile
  }
  return false
}

/** Union two dictionaries, preferring existing values if they possess a parent.usr key. */
func += (left: inout ApiNameNodeMap, right: ApiNameNodeMap) {
  for (k, v) in right {
    if left[k] == nil {
      left.updateValue(v, forKey: k)
    } else if let object = left[k], object["parent.usr"] == nil {
      left.updateValue(v, forKey: k)
    }
  }
}

func prettyString(forKind kind: String) -> String {
  if let pretty = [
    // Objective-C
    "sourcekitten.source.lang.objc.decl.protocol": "protocol",
    "sourcekitten.source.lang.objc.decl.typedef": "typedef",
    "sourcekitten.source.lang.objc.decl.method.instance": "method",
    "sourcekitten.source.lang.objc.decl.property": "property",
    "sourcekitten.source.lang.objc.decl.class": "class",
    "sourcekitten.source.lang.objc.decl.constant": "constant",
    "sourcekitten.source.lang.objc.decl.enum": "enum",
    "sourcekitten.source.lang.objc.decl.enumcase": "enum value",

    // Swift
    "source.lang.swift.decl.function.method.instance": "method",
    "source.lang.swift.decl.var.instance": "var",
    "source.lang.swift.decl.class": "class",
    "source.lang.swift.decl.var.static": "static var",
    "source.lang.swift.decl.enum": "enum"
    ][kind] {
    return pretty
  }
  return kind
}

func prettyString(forModificationKind kind: String) -> String {
  switch kind {
  case "key.swift_declaration": return "swift declaration"
  case "key.parsed_declaration": return "declaration"
  default: return kind
  }
}

/** Walk the APINode to the root node. */
{'fraction_non_alphanumeric': 0.12556763451217834, 'fraction_numerical': 0.0035778175313059034, 'mean_word_length': 3.220383275261324, 'pattern_counts': {'":': 27, '<': 5, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 16, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '817519', 'n_tokens_mistral': 4567, 'n_tokens_neox': 4456, 'n_words': 1240}