├── .clang-format ├── .gitignore ├── .gitmodules ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── apple ├── .gitignore ├── diffreport.xcworkspace │ └── contents.xcworkspacedata └── diffreport │ ├── .gitignore │ ├── Package.swift │ ├── README.md │ ├── Sources │ ├── diffreport │ │ └── main.swift │ └── diffreportlib │ │ └── diffreport.swift │ ├── Tests │ ├── LinuxMain.swift │ └── diffreportlibTests │ │ ├── UnitTests.swift │ │ └── XCTestManifests.swift │ └── diffreport.xcodeproj │ └── project.pbxproj ├── java ├── .gitignore └── markdowndiff.py └── src ├── apidiff └── pathapidiff /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | 3 | AllowShortFunctionsOnASingleLine: Inline 4 | AllowShortIfStatementsOnASingleLine: false 5 | AllowShortLoopsOnASingleLine: false 6 | AlwaysBreakBeforeMultilineStrings: false 7 | BinPackParameters: false 8 | ColumnLimit: 0 9 | IndentWrappedFunctionNames: true 10 | ObjCSpaceBeforeProtocolList: true 11 | PointerBindsToType: false 12 | SortIncludes: true 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build output 2 | build/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_party/jardiff"] 2 | path = third_party/jardiff 3 | url = https://github.com/JakeWharton/jardiff.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode10 3 | sudo: false 4 | notifications: 5 | email: false 6 | before_install: 7 | - gem install xcpretty --no-rdoc --no-ri --no-document --quiet 8 | - brew install sourcekitten 9 | script: 10 | - set -o pipefail 11 | - cd apple/diffreport 12 | - swift test 13 | after_success: 14 | - bash <(curl -s https://codecov.io/bash) 15 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of API diff for Objective-C and Swift code authors for copyright purposes. 2 | # 3 | # This does not necessarily list everyone who has contributed code, since in 4 | # some cases, their employer may be the copyright holder. To see the full list 5 | # of contributors, see the revision history with git log. 6 | 7 | Google Inc. 8 | and other contributors 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 2 | 3 | Initial release. 4 | 5 | Supports Swift, Objective-C, and Android API diff generation. 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | 6 | Before we can use your code, you must sign the 7 | [Google Individual Contributor License Agreement] 8 | (https://cla.developers.google.com/about/google-individual) 9 | (CLA), which you can do online. The CLA is necessary mainly because you own the 10 | copyright to your changes, even after your contribution becomes part of our 11 | codebase, so we need your permission to use and distribute your code. We also 12 | need to be sure of various other things—for instance that you'll tell us if you 13 | know that your code infringes on other people's patents. You don't have to sign 14 | the CLA until after you've submitted your code for review and a member has 15 | approved it, but you must do it before we can put your code into our codebase. 16 | Before you start working on a larger contribution, you should get in touch with 17 | us first through the issue tracker with your idea so that we can help out and 18 | possibly guide you. Coordinating up front makes it much easier to avoid 19 | frustration later on. 20 | 21 | ### Code reviews 22 | 23 | All submissions, including submissions by project members, require review. 24 | We use GitHub pull requests for this purpose. 25 | 26 | ### The small print 27 | 28 | Contributions made by corporations are covered by a different agreement than 29 | the one above, the 30 | [Software Grant and Corporate Contributor License Agreement] 31 | (https://cla.developers.google.com/about/google-corporate). 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # API diff 2 | 3 | [![Build Status](https://travis-ci.org/material-motion/material-motion-apidiff.svg?branch=develop)](https://travis-ci.org/material-motion/material-motion-apidiff) 4 | [![codecov](https://codecov.io/gh/material-motion/material-motion-apidiff/branch/develop/graph/badge.svg)](https://codecov.io/gh/material-motion/material-motion-apidiff) 5 | 6 | An API diff tool for Objective-C, Swift, and Android code that outputs markdown. 7 | 8 | ## Requirements 9 | 10 | Objective-C and Swift engines both require: 11 | 12 | - Xcode 8 13 | - [SourceKitten](https://github.com/jpsim/SourceKitten) 14 | 15 | ## Usage 16 | 17 | Run `apidiff` from within a git repository like so: 18 | 19 | apidiff objc 20 | apidiff swift 21 | apidiff android 22 | 23 | Note that all paths must be relative to the root of the git repository. For example: 24 | 25 | apidiff v1.0.0 v2.0.0 objc src/MaterialMotionRuntime.h 26 | 27 | # Example output 28 | 29 | Auto-generated by running: 30 | 31 | apidiff 734d43e406f53143c2cf8440f43d858d125f0a11 6f7a52744751e511d0daf119642446c46bed1f5c objc src/MaterialMotionRuntime.h 32 | 33 | ## NewClass 34 | 35 | *new* class: `NewClass` 36 | 37 | *new* constructor: `NewClass()` 38 | 39 | *new* field: `text` 40 | 41 | *new* method: `getText()` 42 | 43 | ## MDMPlanPerforming 44 | 45 | *removed* method: `-addPlan:` in `MDMPlanPerforming` 46 | 47 | *modified* protocol: `MDMPlanPerforming` 48 | 49 | | Type | swift declaration | 50 | |---|---| 51 | | From | `protocol MDMPlanPerforming : MDMPerforming` | 52 | | To | `protocol PlanPerforming : Performing` | 53 | 54 | ## MDMScheduler 55 | 56 | *modified* property: `delegate` in `MDMScheduler` 57 | 58 | | Type | swift declaration | 59 | |---|---| 60 | | From | `weak var delegate: MDMSchedulerDelegate? { get set }` | 61 | | To | `weak var delegate: SchedulerDelegate? { get set }` | 62 | 63 | *modified* class: `MDMScheduler` 64 | 65 | | Type | swift declaration | 66 | |---|---| 67 | | From | `class MDMScheduler : NSObject` | 68 | | To | `class Scheduler : NSObject` | 69 | 70 | ## License 71 | 72 | Licensed under the Apache 2.0 license. See LICENSE for details. 73 | -------------------------------------------------------------------------------- /apple/.gitignore: -------------------------------------------------------------------------------- 1 | # Jazzy 2 | docs/ 3 | 4 | # Xcode 5 | # 6 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData/ 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xcuserstate 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | Pods/ 50 | *.xcworkspace 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots 69 | fastlane/test_output 70 | 71 | -------------------------------------------------------------------------------- /apple/diffreport.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /apple/diffreport/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /apple/diffreport/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "diffreport", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .executable( 11 | name: "diffreport", 12 | targets: ["diffreport"] 13 | ), 14 | .library( 15 | name: "diffreportlib", 16 | targets: ["diffreportlib"] 17 | ), 18 | ], 19 | dependencies: [ 20 | // Dependencies declare other packages that this package depends on. 21 | // .package(url: /* package url */, from: "1.0.0"), 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 26 | .target( 27 | name: "diffreport", 28 | dependencies: [ 29 | "diffreportlib" 30 | ] 31 | ), 32 | .target( 33 | name: "diffreportlib", 34 | dependencies: [] 35 | ), 36 | .testTarget( 37 | name: "diffreportlibTests", 38 | dependencies: [ 39 | "diffreportlib" 40 | ] 41 | ), 42 | ] 43 | ) 44 | -------------------------------------------------------------------------------- /apple/diffreport/README.md: -------------------------------------------------------------------------------- 1 | # diffreport 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /apple/diffreport/Sources/diffreport/main.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import diffreportlib 19 | 20 | if ProcessInfo.processInfo.arguments.count < 3 { 21 | print("usage: diffreport ") 22 | exit(1) 23 | } 24 | 25 | /** Load a file from disk, parse it as JSON, and return the result. */ 26 | func readJsonObject(fromFilePath path: String) throws -> Any { 27 | let url = URL(fileURLWithPath: path) 28 | let options = JSONSerialization.ReadingOptions(rawValue: 0) 29 | let data = try Data(contentsOf: url) 30 | if data.count > 0 { 31 | return try JSONSerialization.jsonObject(with: data, options: options) 32 | } else { 33 | return [] 34 | } 35 | } 36 | 37 | let oldApi = try readJsonObject(fromFilePath: ProcessInfo.processInfo.arguments[1]) 38 | let newApi = try readJsonObject(fromFilePath: ProcessInfo.processInfo.arguments[2]) 39 | 40 | let report = try diffreport(oldApi: oldApi, newApi: newApi) 41 | 42 | // Generate markdown output 43 | 44 | for (symbol, entries) in report { 45 | print("\n#### \(symbol)\n") 46 | print(entries.map({ change in change.toMarkdown() }).joined(separator: "\n\n")) 47 | } 48 | -------------------------------------------------------------------------------- /apple/diffreport/Sources/diffreportlib/diffreport.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | 19 | public typealias JSONObject = Any 20 | 21 | typealias SourceKittenNode = [String: Any] 22 | typealias APINode = [String: Any] 23 | typealias ApiNameNodeMap = [String: APINode] 24 | 25 | /** A type of API change. */ 26 | public enum ApiChange { 27 | case addition(apiType: String, name: String) 28 | case deletion(apiType: String, name: String) 29 | case modification(apiType: String, name: String, modificationType: String, from: String, to: String) 30 | } 31 | 32 | /** Generates an API diff report from two SourceKitten JSON outputs. */ 33 | public func diffreport(oldApi: JSONObject, newApi: JSONObject) throws -> [String: [ApiChange]] { 34 | let oldApiNameNodeMap = extractAPINodeMap(from: oldApi as! [SourceKittenNode]) 35 | let newApiNameNodeMap = extractAPINodeMap(from: newApi as! [SourceKittenNode]) 36 | 37 | let oldApiNames = Set(oldApiNameNodeMap.keys) 38 | let newApiNames = Set(newApiNameNodeMap.keys) 39 | 40 | let addedApiNames = newApiNames.subtracting(oldApiNames) 41 | let deletedApiNames = oldApiNames.subtracting(newApiNames) 42 | let persistedApiNames = oldApiNames.intersection(newApiNames) 43 | 44 | var changes: [String: [ApiChange]] = [:] 45 | 46 | // Additions 47 | 48 | for usr in (addedApiNames.map { usr in newApiNameNodeMap[usr]! }.sorted(by: apiNodeIsOrderedBefore)) { 49 | let apiType = prettyString(forKind: usr["key.kind"] as! String) 50 | let name = prettyName(forApi: usr, apis: newApiNameNodeMap) 51 | let root = rootName(forApi: usr, apis: newApiNameNodeMap) 52 | changes[root, withDefault: []].append(.addition(apiType: apiType, name: name)) 53 | } 54 | 55 | // Deletions 56 | 57 | for usr in (deletedApiNames.map { usr in oldApiNameNodeMap[usr]! }.sorted(by: apiNodeIsOrderedBefore)) { 58 | let apiType = prettyString(forKind: usr["key.kind"] as! String) 59 | let name = prettyName(forApi: usr, apis: oldApiNameNodeMap) 60 | let root = rootName(forApi: usr, apis: oldApiNameNodeMap) 61 | changes[root, withDefault: []].append(.deletion(apiType: apiType, name: name)) 62 | } 63 | 64 | // Modifications 65 | 66 | let ignoredKeys = Set(arrayLiteral: "key.doc.line", "key.parsed_scope.end", "key.parsed_scope.start", "key.doc.column", "key.doc.comment", "key.bodyoffset", "key.nameoffset", "key.doc.full_as_xml", "key.offset", "key.fully_annotated_decl", "key.length", "key.bodylength", "key.namelength", "key.annotated_decl", "key.doc.parameters", "key.elements", "key.related_decls") 67 | 68 | for usr in persistedApiNames { 69 | let oldApi = oldApiNameNodeMap[usr]! 70 | let newApi = newApiNameNodeMap[usr]! 71 | let root = rootName(forApi: newApi, apis: newApiNameNodeMap) 72 | let allKeys = Set(oldApi.keys).union(Set(newApi.keys)) 73 | 74 | for key in allKeys { 75 | if ignoredKeys.contains(key) { 76 | continue 77 | } 78 | if let oldValue = oldApi[key] as? String, let newValue = newApi[key] as? String, oldValue != newValue { 79 | let apiType = prettyString(forKind: newApi["key.kind"] as! String) 80 | let name = prettyName(forApi: newApi, apis: newApiNameNodeMap) 81 | let modificationType = prettyString(forModificationKind: key) 82 | if apiType == "class" && key == "key.parsed_declaration" { 83 | // Ignore declarations for classes because it's a complete representation of the class's 84 | // code, which is not helpful diff information. 85 | continue 86 | } 87 | changes[root, withDefault: []].append(.modification(apiType: apiType, 88 | name: name, 89 | modificationType: modificationType, 90 | from: oldValue, 91 | to: newValue)) 92 | } 93 | } 94 | } 95 | 96 | return changes 97 | } 98 | 99 | extension ApiChange { 100 | public func toMarkdown() -> String { 101 | switch self { 102 | case .addition(let apiType, let name): 103 | return "*new* \(apiType): \(name)" 104 | case .deletion(let apiType, let name): 105 | return "*removed* \(apiType): \(name)" 106 | case .modification(let apiType, let name, let modificationType, let from, let to): 107 | return [ 108 | "*modified* \(apiType): \(name)", 109 | "", 110 | "| Type of change: | \(modificationType) |", 111 | "|---|---|", 112 | "| From: | `\(from.replacingOccurrences(of: "\n", with: " "))` |", 113 | "| To: | `\(to.replacingOccurrences(of: "\n", with: " "))` |" 114 | ].joined(separator: "\n") 115 | } 116 | } 117 | } 118 | 119 | extension ApiChange: Equatable {} 120 | 121 | public func == (left: ApiChange, right: ApiChange) -> Bool { 122 | switch (left, right) { 123 | case (let .addition(apiType, name), let .addition(apiType2, name2)): 124 | return apiType == apiType2 && name == name2 125 | case (let .deletion(apiType, name), let .deletion(apiType2, name2)): 126 | return apiType == apiType2 && name == name2 127 | case (let .modification(apiType, name, modificationType, from, to), 128 | let .modification(apiType2, name2, modificationType2, from2, to2)): 129 | return apiType == apiType2 && name == name2 && modificationType == modificationType2 && from == from2 && to == to2 130 | default: 131 | return false 132 | } 133 | } 134 | 135 | /** 136 | get-with-default API for Dictionary 137 | 138 | Example usage: dict[key, withDefault: []] 139 | */ 140 | extension Dictionary { 141 | subscript(key: Key, withDefault value: @autoclosure () -> Value) -> Value { 142 | mutating get { 143 | if self[key] == nil { 144 | self[key] = value() 145 | } 146 | return self[key]! 147 | } 148 | set { 149 | self[key] = newValue 150 | } 151 | } 152 | } 153 | 154 | /** 155 | Sorting function for APINode instances. 156 | 157 | Sorts by filename. 158 | 159 | Example usage: sorted(by: apiNodeIsOrderedBefore) 160 | */ 161 | func apiNodeIsOrderedBefore(prev: APINode, next: APINode) -> Bool { 162 | if let prevFile = prev["key.doc.file"] as? String, let nextFile = next["key.doc.file"] as? String { 163 | return prevFile < nextFile 164 | } 165 | return false 166 | } 167 | 168 | /** Union two dictionaries, preferring existing values if they possess a parent.usr key. */ 169 | func += (left: inout ApiNameNodeMap, right: ApiNameNodeMap) { 170 | for (k, v) in right { 171 | if left[k] == nil { 172 | left.updateValue(v, forKey: k) 173 | } else if let object = left[k], object["parent.usr"] == nil { 174 | left.updateValue(v, forKey: k) 175 | } 176 | } 177 | } 178 | 179 | func prettyString(forKind kind: String) -> String { 180 | if let pretty = [ 181 | // Objective-C 182 | "sourcekitten.source.lang.objc.decl.protocol": "protocol", 183 | "sourcekitten.source.lang.objc.decl.typedef": "typedef", 184 | "sourcekitten.source.lang.objc.decl.method.instance": "method", 185 | "sourcekitten.source.lang.objc.decl.property": "property", 186 | "sourcekitten.source.lang.objc.decl.class": "class", 187 | "sourcekitten.source.lang.objc.decl.constant": "constant", 188 | "sourcekitten.source.lang.objc.decl.enum": "enum", 189 | "sourcekitten.source.lang.objc.decl.enumcase": "enum value", 190 | "sourcekitten.source.lang.objc.decl.category": "category", 191 | "sourcekitten.source.lang.objc.decl.method.class": "class method", 192 | "sourcekitten.source.lang.objc.decl.struct": "struct", 193 | "sourcekitten.source.lang.objc.decl.field": "field", 194 | 195 | // Swift 196 | "source.lang.swift.decl.function.method.static": "static method", 197 | "source.lang.swift.decl.function.method.instance": "method", 198 | "source.lang.swift.decl.var.instance": "var", 199 | "source.lang.swift.decl.class": "class", 200 | "source.lang.swift.decl.var.static": "static var", 201 | "source.lang.swift.decl.enum": "enum", 202 | "source.lang.swift.decl.function.free": "function", 203 | "source.lang.swift.decl.var.global": "global var", 204 | "source.lang.swift.decl.protocol": "protocol", 205 | "source.lang.swift.decl.enumelement": "enum value" 206 | ][kind] { 207 | return pretty 208 | } 209 | return kind 210 | } 211 | 212 | func prettyString(forModificationKind kind: String) -> String { 213 | switch kind { 214 | case "key.swift_declaration": return "Swift declaration" 215 | case "key.parsed_declaration": return "Declaration" 216 | case "key.doc.declaration": return "Declaration" 217 | case "key.typename": return "Declaration" 218 | case "key.always_deprecated": return "Deprecation" 219 | case "key.deprecation_message": return "Deprecation message" 220 | default: return kind 221 | } 222 | } 223 | 224 | /** Walk the APINode to the root node. */ 225 | func rootName(forApi api: APINode, apis: ApiNameNodeMap) -> String { 226 | let name = api["key.name"] as! String 227 | if let parentUsr = api["parent.usr"] as? String, let parentApi = apis[parentUsr] { 228 | return rootName(forApi: parentApi, apis: apis) 229 | } 230 | return name 231 | } 232 | 233 | func prettyName(forApi api: APINode, apis: ApiNameNodeMap) -> String { 234 | let name = api["key.name"] as! String 235 | if let parentUsr = api["parent.usr"] as? String, let parentApi = apis[parentUsr] { 236 | return "`\(name)` in \(prettyName(forApi: parentApi, apis: apis))" 237 | } 238 | return "`\(name)`" 239 | } 240 | 241 | /** Normalize data contained in an API node json dictionary. */ 242 | func apiNode(from sourceKittenNode: SourceKittenNode) -> APINode { 243 | var data = sourceKittenNode 244 | data.removeValue(forKey: "key.substructure") 245 | for (key, value) in data { 246 | data[key] = String(describing: value) 247 | } 248 | return data 249 | } 250 | 251 | /** 252 | Recursively iterate over each sourcekitten node and extract a flattened map of USR identifier to 253 | APINode instance. 254 | */ 255 | func extractAPINodeMap(from sourceKittenNodes: [SourceKittenNode]) -> ApiNameNodeMap { 256 | var map: ApiNameNodeMap = [:] 257 | for file in sourceKittenNodes { 258 | for (_, information) in file { 259 | let substructure = (information as! SourceKittenNode)["key.substructure"] as! [SourceKittenNode] 260 | for jsonNode in substructure { 261 | map += extractAPINodeMap(from: jsonNode) 262 | } 263 | } 264 | } 265 | return map 266 | } 267 | 268 | /** 269 | Recursively iterate over a sourcekitten node and extract a flattened map of USR identifier to 270 | APINode instance. 271 | */ 272 | func extractAPINodeMap(from sourceKittenNode: SourceKittenNode, parentUsr: String? = nil) -> ApiNameNodeMap { 273 | var map: ApiNameNodeMap = [:] 274 | for (key, value) in sourceKittenNode { 275 | switch key { 276 | case "key.usr": 277 | if let accessibility = sourceKittenNode["key.accessibility"] { 278 | if accessibility as! String != "source.lang.swift.accessibility.public" { 279 | continue 280 | } 281 | } else if let kind = sourceKittenNode["key.kind"] as? String, kind == "source.lang.swift.decl.extension" { 282 | continue 283 | } 284 | var node = apiNode(from: sourceKittenNode) 285 | 286 | // Create a reference to the parent node 287 | node["parent.usr"] = parentUsr 288 | 289 | // Store the API node in the map 290 | map[value as! String] = node 291 | 292 | case "key.substructure": 293 | let substructure = value as! [SourceKittenNode] 294 | for subSourceKittenNode in substructure { 295 | map += extractAPINodeMap(from: subSourceKittenNode, parentUsr: sourceKittenNode["key.usr"] as? String) 296 | } 297 | default: 298 | continue 299 | } 300 | } 301 | return map 302 | } 303 | 304 | /** 305 | Execute sourcekitten with a given umbrella header. 306 | 307 | Only meant to be used in unit test builds. 308 | 309 | @param header Absolute path to an umbrella header. 310 | */ 311 | func runSourceKitten(withHeader header: String) throws -> JSONObject { 312 | let task = Process() 313 | task.launchPath = "/usr/bin/env" 314 | task.arguments = [ 315 | "/usr/local/bin/sourcekitten", 316 | "doc", 317 | "--objc", 318 | header, 319 | "--", 320 | "-x", 321 | "objective-c", 322 | ] 323 | let standardOutput = Pipe() 324 | task.standardOutput = standardOutput 325 | task.launch() 326 | task.waitUntilExit() 327 | var data = standardOutput.fileHandleForReading.readDataToEndOfFile() 328 | let tmpDir = ProcessInfo.processInfo.environment["TMPDIR"]!.replacingOccurrences(of: "/", with: "\\/") 329 | let string = String(data: data, encoding: String.Encoding.utf8)! 330 | .replacingOccurrences(of: tmpDir + "old\\/", with: "") 331 | .replacingOccurrences(of: tmpDir + "new\\/", with: "") 332 | data = string.data(using: String.Encoding.utf8)! 333 | return try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions(rawValue: 0)) 334 | } 335 | -------------------------------------------------------------------------------- /apple/diffreport/Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import libTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += UnitTests.allTests() 7 | XCTMain(tests) -------------------------------------------------------------------------------- /apple/diffreport/Tests/diffreportlibTests/UnitTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | @testable import diffreportlib 19 | 20 | // These tests require sourcekitten to be installed as a command line tool. 21 | // brew install sourcekitten 22 | class UnitTests: XCTestCase { 23 | 24 | static var allTests = [ 25 | ("testNoChanges", testNoChanges), 26 | ("testAddition", testAddition), 27 | ("testDeletion", testDeletion), 28 | ("testModification", testModification), 29 | ] 30 | 31 | func testNoChanges() throws { 32 | let report = try generateReport(forOld: """ 33 | @interface TestObject 34 | @end 35 | """, 36 | new: """ 37 | @interface TestObject 38 | @end 39 | """) 40 | XCTAssertEqual(report.count, 0) 41 | } 42 | 43 | func testAddition() throws { 44 | let report = try generateReport(forOld: """ 45 | """, new: """ 46 | @interface TestObject 47 | @end 48 | """) 49 | XCTAssertEqual(report["TestObject"]!.count, 1) 50 | XCTAssertEqual(report["TestObject"]!.first, ApiChange.addition(apiType: "class", name: "`TestObject`")) 51 | } 52 | 53 | func testDeletion() throws { 54 | let report = try generateReport(forOld: """ 55 | @interface TestObject 56 | @end 57 | """, new: """ 58 | """) 59 | XCTAssertEqual(report["TestObject"]!.count, 1) 60 | XCTAssertEqual(report["TestObject"]!.first, ApiChange.deletion(apiType: "class", name: "`TestObject`")) 61 | } 62 | 63 | func testModification() throws { 64 | let report = try generateReport(forOld: """ 65 | /** Docs */ 66 | @interface TestObject 67 | 68 | @property(nonatomic) id object; 69 | 70 | @end 71 | """, new: """ 72 | /** Docs */ 73 | @interface TestObject 74 | 75 | @property(atomic) id object; 76 | 77 | @end 78 | """) 79 | XCTAssertEqual(report["TestObject"]!.count, 1) 80 | XCTAssertEqual(report["TestObject"]!.first!, 81 | ApiChange.modification(apiType: "property", 82 | name: "`object` in `TestObject`", 83 | modificationType: "Declaration", 84 | from: "@property(nonatomic) id object", 85 | to: "@property(atomic) id object")) 86 | } 87 | 88 | func testNewPropertyOnClass() throws { 89 | let report = try generateReport(forOld: """ 90 | @interface MDCAlertControllerView 91 | @end 92 | """, 93 | new: """ 94 | @interface MDCAlertControllerView 95 | @property(nonatomic, strong, nullable) id buttonInkColor; 96 | @end 97 | """) 98 | XCTAssertEqual(report["MDCAlertControllerView"]!.count, 1) 99 | XCTAssertEqual(report["MDCAlertControllerView"]!.first!, 100 | .addition(apiType: "property", 101 | name: "`buttonInkColor` in `MDCAlertControllerView`")) 102 | } 103 | 104 | let oldPath = ProcessInfo.processInfo.environment["TMPDIR"]!.appending("old/Header.h") 105 | let newPath = ProcessInfo.processInfo.environment["TMPDIR"]!.appending("new/Header.h") 106 | 107 | override func setUp() { 108 | do { 109 | try FileManager.default.createDirectory(atPath: ProcessInfo.processInfo.environment["TMPDIR"]!.appending("old"), withIntermediateDirectories: true, attributes: nil) 110 | try FileManager.default.createDirectory(atPath: ProcessInfo.processInfo.environment["TMPDIR"]!.appending("new"), withIntermediateDirectories: true, attributes: nil) 111 | } catch { 112 | 113 | } 114 | } 115 | 116 | func generateReport(forOld old: String, new: String) throws -> [String: [ApiChange]] { 117 | try old.write(toFile: oldPath, atomically: true, encoding: String.Encoding.utf8) 118 | try new.write(toFile: newPath, atomically: true, encoding: String.Encoding.utf8) 119 | let oldApi = try runSourceKitten(withHeader: oldPath) 120 | let newApi = try runSourceKitten(withHeader: newPath) 121 | return try diffreport(oldApi: oldApi, newApi: newApi) 122 | } 123 | } 124 | 125 | -------------------------------------------------------------------------------- /apple/diffreport/Tests/diffreportlibTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !os(macOS) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(libTests.allTests), 7 | ] 8 | } 9 | #endif -------------------------------------------------------------------------------- /apple/diffreport/diffreport.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXAggregateTarget section */ 10 | "diffreport::diffreportPackageTests::ProductTarget" /* diffreportPackageTests */ = { 11 | isa = PBXAggregateTarget; 12 | buildConfigurationList = OBJ_53 /* Build configuration list for PBXAggregateTarget "diffreportPackageTests" */; 13 | buildPhases = ( 14 | ); 15 | dependencies = ( 16 | OBJ_56 /* PBXTargetDependency */, 17 | ); 18 | name = diffreportPackageTests; 19 | productName = diffreportPackageTests; 20 | }; 21 | /* End PBXAggregateTarget section */ 22 | 23 | /* Begin PBXBuildFile section */ 24 | OBJ_25 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; 25 | OBJ_31 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* main.swift */; }; 26 | OBJ_33 /* diffreportlib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "diffreport::diffreportlib::Product" /* diffreportlib.framework */; }; 27 | OBJ_41 /* UnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* UnitTests.swift */; }; 28 | OBJ_42 /* XCTestManifests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* XCTestManifests.swift */; }; 29 | OBJ_44 /* diffreportlib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "diffreport::diffreportlib::Product" /* diffreportlib.framework */; }; 30 | OBJ_50 /* diffreport.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* diffreport.swift */; }; 31 | /* End PBXBuildFile section */ 32 | 33 | /* Begin PBXContainerItemProxy section */ 34 | 66354EE1212DE00400DD4416 /* PBXContainerItemProxy */ = { 35 | isa = PBXContainerItemProxy; 36 | containerPortal = OBJ_1 /* Project object */; 37 | proxyType = 1; 38 | remoteGlobalIDString = "diffreport::diffreportlib"; 39 | remoteInfo = diffreportlib; 40 | }; 41 | 66354EE2212DE00500DD4416 /* PBXContainerItemProxy */ = { 42 | isa = PBXContainerItemProxy; 43 | containerPortal = OBJ_1 /* Project object */; 44 | proxyType = 1; 45 | remoteGlobalIDString = "diffreport::diffreportlib"; 46 | remoteInfo = diffreportlib; 47 | }; 48 | 66354EE3212DE00500DD4416 /* PBXContainerItemProxy */ = { 49 | isa = PBXContainerItemProxy; 50 | containerPortal = OBJ_1 /* Project object */; 51 | proxyType = 1; 52 | remoteGlobalIDString = "diffreport::diffreportlibTests"; 53 | remoteInfo = diffreportlibTests; 54 | }; 55 | /* End PBXContainerItemProxy section */ 56 | 57 | /* Begin PBXFileReference section */ 58 | OBJ_11 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 59 | OBJ_14 /* UnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitTests.swift; sourceTree = ""; }; 60 | OBJ_15 /* XCTestManifests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestManifests.swift; sourceTree = ""; }; 61 | OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 62 | OBJ_9 /* diffreport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = diffreport.swift; sourceTree = ""; }; 63 | "diffreport::diffreport::Product" /* diffreport */ = {isa = PBXFileReference; lastKnownFileType = text; path = diffreport; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | "diffreport::diffreportlib::Product" /* diffreportlib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = diffreportlib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | "diffreport::diffreportlibTests::Product" /* diffreportlibTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = diffreportlibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 66 | /* End PBXFileReference section */ 67 | 68 | /* Begin PBXFrameworksBuildPhase section */ 69 | OBJ_32 /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 0; 72 | files = ( 73 | OBJ_33 /* diffreportlib.framework in Frameworks */, 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | OBJ_43 /* Frameworks */ = { 78 | isa = PBXFrameworksBuildPhase; 79 | buildActionMask = 0; 80 | files = ( 81 | OBJ_44 /* diffreportlib.framework in Frameworks */, 82 | ); 83 | runOnlyForDeploymentPostprocessing = 0; 84 | }; 85 | OBJ_51 /* Frameworks */ = { 86 | isa = PBXFrameworksBuildPhase; 87 | buildActionMask = 0; 88 | files = ( 89 | ); 90 | runOnlyForDeploymentPostprocessing = 0; 91 | }; 92 | /* End PBXFrameworksBuildPhase section */ 93 | 94 | /* Begin PBXGroup section */ 95 | OBJ_10 /* diffreport */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | OBJ_11 /* main.swift */, 99 | ); 100 | name = diffreport; 101 | path = Sources/diffreport; 102 | sourceTree = SOURCE_ROOT; 103 | }; 104 | OBJ_12 /* Tests */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | OBJ_13 /* diffreportlibTests */, 108 | ); 109 | name = Tests; 110 | sourceTree = SOURCE_ROOT; 111 | }; 112 | OBJ_13 /* diffreportlibTests */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | OBJ_14 /* UnitTests.swift */, 116 | OBJ_15 /* XCTestManifests.swift */, 117 | ); 118 | name = diffreportlibTests; 119 | path = Tests/diffreportlibTests; 120 | sourceTree = SOURCE_ROOT; 121 | }; 122 | OBJ_16 /* Products */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | "diffreport::diffreport::Product" /* diffreport */, 126 | "diffreport::diffreportlibTests::Product" /* diffreportlibTests.xctest */, 127 | "diffreport::diffreportlib::Product" /* diffreportlib.framework */, 128 | ); 129 | name = Products; 130 | sourceTree = BUILT_PRODUCTS_DIR; 131 | }; 132 | OBJ_5 /* */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | OBJ_6 /* Package.swift */, 136 | OBJ_7 /* Sources */, 137 | OBJ_12 /* Tests */, 138 | OBJ_16 /* Products */, 139 | ); 140 | name = ""; 141 | sourceTree = ""; 142 | }; 143 | OBJ_7 /* Sources */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | OBJ_8 /* diffreportlib */, 147 | OBJ_10 /* diffreport */, 148 | ); 149 | name = Sources; 150 | sourceTree = SOURCE_ROOT; 151 | }; 152 | OBJ_8 /* diffreportlib */ = { 153 | isa = PBXGroup; 154 | children = ( 155 | OBJ_9 /* diffreport.swift */, 156 | ); 157 | name = diffreportlib; 158 | path = Sources/diffreportlib; 159 | sourceTree = SOURCE_ROOT; 160 | }; 161 | /* End PBXGroup section */ 162 | 163 | /* Begin PBXNativeTarget section */ 164 | "diffreport::SwiftPMPackageDescription" /* diffreportPackageDescription */ = { 165 | isa = PBXNativeTarget; 166 | buildConfigurationList = OBJ_21 /* Build configuration list for PBXNativeTarget "diffreportPackageDescription" */; 167 | buildPhases = ( 168 | OBJ_24 /* Sources */, 169 | ); 170 | buildRules = ( 171 | ); 172 | dependencies = ( 173 | ); 174 | name = diffreportPackageDescription; 175 | productName = diffreportPackageDescription; 176 | productType = "com.apple.product-type.framework"; 177 | }; 178 | "diffreport::diffreport" /* diffreport */ = { 179 | isa = PBXNativeTarget; 180 | buildConfigurationList = OBJ_27 /* Build configuration list for PBXNativeTarget "diffreport" */; 181 | buildPhases = ( 182 | OBJ_30 /* Sources */, 183 | OBJ_32 /* Frameworks */, 184 | ); 185 | buildRules = ( 186 | ); 187 | dependencies = ( 188 | OBJ_34 /* PBXTargetDependency */, 189 | ); 190 | name = diffreport; 191 | productName = diffreport; 192 | productReference = "diffreport::diffreport::Product" /* diffreport */; 193 | productType = "com.apple.product-type.tool"; 194 | }; 195 | "diffreport::diffreportlib" /* diffreportlib */ = { 196 | isa = PBXNativeTarget; 197 | buildConfigurationList = OBJ_46 /* Build configuration list for PBXNativeTarget "diffreportlib" */; 198 | buildPhases = ( 199 | OBJ_49 /* Sources */, 200 | OBJ_51 /* Frameworks */, 201 | ); 202 | buildRules = ( 203 | ); 204 | dependencies = ( 205 | ); 206 | name = diffreportlib; 207 | productName = diffreportlib; 208 | productReference = "diffreport::diffreportlib::Product" /* diffreportlib.framework */; 209 | productType = "com.apple.product-type.framework"; 210 | }; 211 | "diffreport::diffreportlibTests" /* diffreportlibTests */ = { 212 | isa = PBXNativeTarget; 213 | buildConfigurationList = OBJ_37 /* Build configuration list for PBXNativeTarget "diffreportlibTests" */; 214 | buildPhases = ( 215 | OBJ_40 /* Sources */, 216 | OBJ_43 /* Frameworks */, 217 | ); 218 | buildRules = ( 219 | ); 220 | dependencies = ( 221 | OBJ_45 /* PBXTargetDependency */, 222 | ); 223 | name = diffreportlibTests; 224 | productName = diffreportlibTests; 225 | productReference = "diffreport::diffreportlibTests::Product" /* diffreportlibTests.xctest */; 226 | productType = "com.apple.product-type.bundle.unit-test"; 227 | }; 228 | /* End PBXNativeTarget section */ 229 | 230 | /* Begin PBXProject section */ 231 | OBJ_1 /* Project object */ = { 232 | isa = PBXProject; 233 | attributes = { 234 | LastUpgradeCheck = 9999; 235 | }; 236 | buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "diffreport" */; 237 | compatibilityVersion = "Xcode 3.2"; 238 | developmentRegion = English; 239 | hasScannedForEncodings = 0; 240 | knownRegions = ( 241 | en, 242 | ); 243 | mainGroup = OBJ_5 /* */; 244 | productRefGroup = OBJ_16 /* Products */; 245 | projectDirPath = ""; 246 | projectRoot = ""; 247 | targets = ( 248 | "diffreport::SwiftPMPackageDescription" /* diffreportPackageDescription */, 249 | "diffreport::diffreport" /* diffreport */, 250 | "diffreport::diffreportlibTests" /* diffreportlibTests */, 251 | "diffreport::diffreportlib" /* diffreportlib */, 252 | "diffreport::diffreportPackageTests::ProductTarget" /* diffreportPackageTests */, 253 | ); 254 | }; 255 | /* End PBXProject section */ 256 | 257 | /* Begin PBXSourcesBuildPhase section */ 258 | OBJ_24 /* Sources */ = { 259 | isa = PBXSourcesBuildPhase; 260 | buildActionMask = 0; 261 | files = ( 262 | OBJ_25 /* Package.swift in Sources */, 263 | ); 264 | runOnlyForDeploymentPostprocessing = 0; 265 | }; 266 | OBJ_30 /* Sources */ = { 267 | isa = PBXSourcesBuildPhase; 268 | buildActionMask = 0; 269 | files = ( 270 | OBJ_31 /* main.swift in Sources */, 271 | ); 272 | runOnlyForDeploymentPostprocessing = 0; 273 | }; 274 | OBJ_40 /* Sources */ = { 275 | isa = PBXSourcesBuildPhase; 276 | buildActionMask = 0; 277 | files = ( 278 | OBJ_41 /* UnitTests.swift in Sources */, 279 | OBJ_42 /* XCTestManifests.swift in Sources */, 280 | ); 281 | runOnlyForDeploymentPostprocessing = 0; 282 | }; 283 | OBJ_49 /* Sources */ = { 284 | isa = PBXSourcesBuildPhase; 285 | buildActionMask = 0; 286 | files = ( 287 | OBJ_50 /* diffreport.swift in Sources */, 288 | ); 289 | runOnlyForDeploymentPostprocessing = 0; 290 | }; 291 | /* End PBXSourcesBuildPhase section */ 292 | 293 | /* Begin PBXTargetDependency section */ 294 | OBJ_34 /* PBXTargetDependency */ = { 295 | isa = PBXTargetDependency; 296 | target = "diffreport::diffreportlib" /* diffreportlib */; 297 | targetProxy = 66354EE2212DE00500DD4416 /* PBXContainerItemProxy */; 298 | }; 299 | OBJ_45 /* PBXTargetDependency */ = { 300 | isa = PBXTargetDependency; 301 | target = "diffreport::diffreportlib" /* diffreportlib */; 302 | targetProxy = 66354EE1212DE00400DD4416 /* PBXContainerItemProxy */; 303 | }; 304 | OBJ_56 /* PBXTargetDependency */ = { 305 | isa = PBXTargetDependency; 306 | target = "diffreport::diffreportlibTests" /* diffreportlibTests */; 307 | targetProxy = 66354EE3212DE00500DD4416 /* PBXContainerItemProxy */; 308 | }; 309 | /* End PBXTargetDependency section */ 310 | 311 | /* Begin XCBuildConfiguration section */ 312 | OBJ_22 /* Debug */ = { 313 | isa = XCBuildConfiguration; 314 | buildSettings = { 315 | LD = /usr/bin/true; 316 | OTHER_SWIFT_FLAGS = "-swift-version 4 -I /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/pm/4 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk"; 317 | SWIFT_VERSION = 4.0; 318 | }; 319 | name = Debug; 320 | }; 321 | OBJ_23 /* Release */ = { 322 | isa = XCBuildConfiguration; 323 | buildSettings = { 324 | LD = /usr/bin/true; 325 | OTHER_SWIFT_FLAGS = "-swift-version 4 -I /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/pm/4 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk"; 326 | SWIFT_VERSION = 4.0; 327 | }; 328 | name = Release; 329 | }; 330 | OBJ_28 /* Debug */ = { 331 | isa = XCBuildConfiguration; 332 | buildSettings = { 333 | FRAMEWORK_SEARCH_PATHS = ( 334 | "$(inherited)", 335 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 336 | ); 337 | HEADER_SEARCH_PATHS = "$(inherited)"; 338 | INFOPLIST_FILE = diffreport.xcodeproj/diffreport_Info.plist; 339 | LD_RUNPATH_SEARCH_PATHS = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx @executable_path"; 340 | OTHER_LDFLAGS = "$(inherited)"; 341 | OTHER_SWIFT_FLAGS = "$(inherited)"; 342 | SWIFT_FORCE_DYNAMIC_LINK_STDLIB = YES; 343 | SWIFT_FORCE_STATIC_LINK_STDLIB = NO; 344 | SWIFT_VERSION = 4.0; 345 | TARGET_NAME = diffreport; 346 | }; 347 | name = Debug; 348 | }; 349 | OBJ_29 /* Release */ = { 350 | isa = XCBuildConfiguration; 351 | buildSettings = { 352 | FRAMEWORK_SEARCH_PATHS = ( 353 | "$(inherited)", 354 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 355 | ); 356 | HEADER_SEARCH_PATHS = "$(inherited)"; 357 | INFOPLIST_FILE = diffreport.xcodeproj/diffreport_Info.plist; 358 | LD_RUNPATH_SEARCH_PATHS = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx @executable_path"; 359 | OTHER_LDFLAGS = "$(inherited)"; 360 | OTHER_SWIFT_FLAGS = "$(inherited)"; 361 | SWIFT_FORCE_DYNAMIC_LINK_STDLIB = YES; 362 | SWIFT_FORCE_STATIC_LINK_STDLIB = NO; 363 | SWIFT_VERSION = 4.0; 364 | TARGET_NAME = diffreport; 365 | }; 366 | name = Release; 367 | }; 368 | OBJ_3 /* Debug */ = { 369 | isa = XCBuildConfiguration; 370 | buildSettings = { 371 | CLANG_ENABLE_OBJC_ARC = YES; 372 | COMBINE_HIDPI_IMAGES = YES; 373 | COPY_PHASE_STRIP = NO; 374 | DEBUG_INFORMATION_FORMAT = dwarf; 375 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 376 | ENABLE_NS_ASSERTIONS = YES; 377 | GCC_OPTIMIZATION_LEVEL = 0; 378 | MACOSX_DEPLOYMENT_TARGET = 10.10; 379 | ONLY_ACTIVE_ARCH = YES; 380 | OTHER_SWIFT_FLAGS = "-DXcode"; 381 | PRODUCT_NAME = "$(TARGET_NAME)"; 382 | SDKROOT = macosx; 383 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 384 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 385 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 386 | USE_HEADERMAP = NO; 387 | }; 388 | name = Debug; 389 | }; 390 | OBJ_38 /* Debug */ = { 391 | isa = XCBuildConfiguration; 392 | buildSettings = { 393 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 394 | FRAMEWORK_SEARCH_PATHS = ( 395 | "$(inherited)", 396 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 397 | ); 398 | HEADER_SEARCH_PATHS = "$(inherited)"; 399 | INFOPLIST_FILE = diffreport.xcodeproj/diffreportlibTests_Info.plist; 400 | LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks @loader_path/Frameworks"; 401 | OTHER_LDFLAGS = "$(inherited)"; 402 | OTHER_SWIFT_FLAGS = "$(inherited)"; 403 | SWIFT_VERSION = 4.0; 404 | TARGET_NAME = diffreportlibTests; 405 | }; 406 | name = Debug; 407 | }; 408 | OBJ_39 /* Release */ = { 409 | isa = XCBuildConfiguration; 410 | buildSettings = { 411 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 412 | FRAMEWORK_SEARCH_PATHS = ( 413 | "$(inherited)", 414 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 415 | ); 416 | HEADER_SEARCH_PATHS = "$(inherited)"; 417 | INFOPLIST_FILE = diffreport.xcodeproj/diffreportlibTests_Info.plist; 418 | LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks @loader_path/Frameworks"; 419 | OTHER_LDFLAGS = "$(inherited)"; 420 | OTHER_SWIFT_FLAGS = "$(inherited)"; 421 | SWIFT_VERSION = 4.0; 422 | TARGET_NAME = diffreportlibTests; 423 | }; 424 | name = Release; 425 | }; 426 | OBJ_4 /* Release */ = { 427 | isa = XCBuildConfiguration; 428 | buildSettings = { 429 | CLANG_ENABLE_OBJC_ARC = YES; 430 | COMBINE_HIDPI_IMAGES = YES; 431 | COPY_PHASE_STRIP = YES; 432 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 433 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 434 | GCC_OPTIMIZATION_LEVEL = s; 435 | MACOSX_DEPLOYMENT_TARGET = 10.10; 436 | OTHER_SWIFT_FLAGS = "-DXcode"; 437 | PRODUCT_NAME = "$(TARGET_NAME)"; 438 | SDKROOT = macosx; 439 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 440 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 441 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 442 | USE_HEADERMAP = NO; 443 | }; 444 | name = Release; 445 | }; 446 | OBJ_47 /* Debug */ = { 447 | isa = XCBuildConfiguration; 448 | buildSettings = { 449 | ENABLE_TESTABILITY = YES; 450 | FRAMEWORK_SEARCH_PATHS = ( 451 | "$(inherited)", 452 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 453 | ); 454 | HEADER_SEARCH_PATHS = "$(inherited)"; 455 | INFOPLIST_FILE = diffreport.xcodeproj/diffreportlib_Info.plist; 456 | LD_RUNPATH_SEARCH_PATHS = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 457 | OTHER_LDFLAGS = "$(inherited)"; 458 | OTHER_SWIFT_FLAGS = "$(inherited)"; 459 | PRODUCT_BUNDLE_IDENTIFIER = diffreportlib; 460 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 461 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 462 | SKIP_INSTALL = YES; 463 | SWIFT_VERSION = 4.0; 464 | TARGET_NAME = diffreportlib; 465 | }; 466 | name = Debug; 467 | }; 468 | OBJ_48 /* Release */ = { 469 | isa = XCBuildConfiguration; 470 | buildSettings = { 471 | ENABLE_TESTABILITY = YES; 472 | FRAMEWORK_SEARCH_PATHS = ( 473 | "$(inherited)", 474 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 475 | ); 476 | HEADER_SEARCH_PATHS = "$(inherited)"; 477 | INFOPLIST_FILE = diffreport.xcodeproj/diffreportlib_Info.plist; 478 | LD_RUNPATH_SEARCH_PATHS = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 479 | OTHER_LDFLAGS = "$(inherited)"; 480 | OTHER_SWIFT_FLAGS = "$(inherited)"; 481 | PRODUCT_BUNDLE_IDENTIFIER = diffreportlib; 482 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 483 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 484 | SKIP_INSTALL = YES; 485 | SWIFT_VERSION = 4.0; 486 | TARGET_NAME = diffreportlib; 487 | }; 488 | name = Release; 489 | }; 490 | OBJ_54 /* Debug */ = { 491 | isa = XCBuildConfiguration; 492 | buildSettings = { 493 | }; 494 | name = Debug; 495 | }; 496 | OBJ_55 /* Release */ = { 497 | isa = XCBuildConfiguration; 498 | buildSettings = { 499 | }; 500 | name = Release; 501 | }; 502 | /* End XCBuildConfiguration section */ 503 | 504 | /* Begin XCConfigurationList section */ 505 | OBJ_2 /* Build configuration list for PBXProject "diffreport" */ = { 506 | isa = XCConfigurationList; 507 | buildConfigurations = ( 508 | OBJ_3 /* Debug */, 509 | OBJ_4 /* Release */, 510 | ); 511 | defaultConfigurationIsVisible = 0; 512 | defaultConfigurationName = Debug; 513 | }; 514 | OBJ_21 /* Build configuration list for PBXNativeTarget "diffreportPackageDescription" */ = { 515 | isa = XCConfigurationList; 516 | buildConfigurations = ( 517 | OBJ_22 /* Debug */, 518 | OBJ_23 /* Release */, 519 | ); 520 | defaultConfigurationIsVisible = 0; 521 | defaultConfigurationName = Debug; 522 | }; 523 | OBJ_27 /* Build configuration list for PBXNativeTarget "diffreport" */ = { 524 | isa = XCConfigurationList; 525 | buildConfigurations = ( 526 | OBJ_28 /* Debug */, 527 | OBJ_29 /* Release */, 528 | ); 529 | defaultConfigurationIsVisible = 0; 530 | defaultConfigurationName = Debug; 531 | }; 532 | OBJ_37 /* Build configuration list for PBXNativeTarget "diffreportlibTests" */ = { 533 | isa = XCConfigurationList; 534 | buildConfigurations = ( 535 | OBJ_38 /* Debug */, 536 | OBJ_39 /* Release */, 537 | ); 538 | defaultConfigurationIsVisible = 0; 539 | defaultConfigurationName = Debug; 540 | }; 541 | OBJ_46 /* Build configuration list for PBXNativeTarget "diffreportlib" */ = { 542 | isa = XCConfigurationList; 543 | buildConfigurations = ( 544 | OBJ_47 /* Debug */, 545 | OBJ_48 /* Release */, 546 | ); 547 | defaultConfigurationIsVisible = 0; 548 | defaultConfigurationName = Debug; 549 | }; 550 | OBJ_53 /* Build configuration list for PBXAggregateTarget "diffreportPackageTests" */ = { 551 | isa = XCConfigurationList; 552 | buildConfigurations = ( 553 | OBJ_54 /* Debug */, 554 | OBJ_55 /* Release */, 555 | ); 556 | defaultConfigurationIsVisible = 0; 557 | defaultConfigurationName = Debug; 558 | }; 559 | /* End XCConfigurationList section */ 560 | }; 561 | rootObject = OBJ_1 /* Project object */; 562 | } 563 | -------------------------------------------------------------------------------- /java/.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | *.pyc 3 | -------------------------------------------------------------------------------- /java/markdowndiff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import re 5 | import subprocess 6 | import sys 7 | from collections import namedtuple 8 | from collections import defaultdict 9 | 10 | # Note: The components of SymbolId should be chosen such that if two SymbolIds 11 | # are not equal, then they should represent two legal symbols. 12 | # 13 | # One consequence of this rule is that you cannot have a separate 14 | # Kind.Klass vs Kind.Interface since that could represent 15 | # class Foo and interface Foo, which would be a name conflict. 16 | SymbolId = namedtuple('SymbolId', 'klass kind signature') 17 | 18 | # Full definition is the entire public api with all decorations. 19 | # Short definition is usually the signature, plus useful human-useful 20 | # identifiers like the method return type, field type, or @ for annotations. 21 | Definition = namedtuple('Definition', 'full short kind_description') 22 | 23 | class Kind: 24 | KLASS, CONSTRUCTOR, FIELD, METHOD = range(1, 5) 25 | 26 | Addition = namedtuple('Addition', 'symbol_id definition') 27 | Deletion = namedtuple('Deletion', 'symbol_id definition') 28 | Modification = namedtuple('Modification', 'symbol_id old_definition new_definition') 29 | 30 | 31 | def print_api_diff(temp_folder, old_path, new_path): 32 | '''Prints the diff between old_path and new_path in markdown format. 33 | 34 | old_path, new_path: 35 | A directory containing one file for each public class. 36 | Each file contains the public APIs of that class. 37 | 38 | temp_folder: 39 | The shared parent directory betwen old_path and new_path. 40 | ''' 41 | 42 | old_symbols = _symbols_for_library(old_path) 43 | new_symbols = _symbols_for_library(new_path) 44 | report = _changes(old_symbols, new_symbols) 45 | _print_markdown(report) 46 | 47 | def _symbols_for_library(path): 48 | '''Parses the library directory and returns a map of SymbolId to Definition. 49 | 50 | Input library path contains: 51 | path 52 | +-- Klass1 53 | +-- Klass2 54 | +-- Klass3 55 | ''' 56 | 57 | symbols = {} # { SymbolId: Definition, ... } 58 | for directory, subdirectories, files in os.walk(path): 59 | for file in files: 60 | symbols.update(_symbols_for_klass(os.path.join(directory, file))) 61 | return symbols 62 | 63 | 64 | def _symbols_for_klass(file): 65 | '''Parses the class file and returns a map of SymbolId to Definition. 66 | 67 | Input class file contains: 68 | public class Klass1 { 69 | public constructor(); 70 | public void method(args) throws Exception; 71 | public int FIELD; 72 | } 73 | ''' 74 | 75 | symbols = {} # { SymbolId: Definition, ... } 76 | 77 | klass_symbol_id = None 78 | klass_definition = None 79 | 80 | with open(file) as f: 81 | lines = f.readlines() 82 | for index, line in enumerate(lines): 83 | if index == len(lines) - 1: 84 | # Class declaration end braces. 85 | continue 86 | 87 | isKlass = index == 0 88 | full_definition = line.strip()[:-1].strip() 89 | (kind, signature, short_definition, kind_description) = _parse_full_definition(full_definition, klass_symbol_id, klass_definition) 90 | 91 | klass_signature = signature if isKlass else klass_symbol_id.signature 92 | 93 | symbol_id = SymbolId(klass_signature, kind, signature) 94 | definition = Definition(full_definition, short_definition, kind_description) 95 | symbols[symbol_id] = definition 96 | 97 | if isKlass: 98 | # Class declaration. 99 | klass_symbol_id = symbol_id 100 | klass_definition = definition 101 | return symbols 102 | 103 | 104 | def _parse_full_definition(full_definition, klass_symbol_id, klass_definition): 105 | '''Parses a symbol's full definition string and returns the (kind, signature, short_definition).''' 106 | 107 | visibility = '(?:public\s+|protected\s+|private\s+)?' 108 | modifiers = '(?:static\s+|abstract\s+|final\s+|native\s+|strictfp\s+|synchronized\s+|transient\s+|volatile\s+)*' 109 | klass_type = '(?:class|interface|enum)' 110 | typed_parameter = '(?:<.*>\s+)?' 111 | object_type = '.+?\s+' # Either the return value type or field type. 112 | throws = '.*' 113 | extends_implements = '.*' 114 | 115 | # Klass 116 | # public abstract class foo.bar.Baz extends qux.Quz 117 | match = re.match('%s(%s)(%s)\s+(\S+)(%s)' % (visibility, modifiers, klass_type, extends_implements), full_definition) 118 | if match: 119 | kind_description = match.group(2) 120 | signature = short_definition = match.group(3) 121 | 122 | # Special processing for annotations. 123 | extends = match.group(4) 124 | if kind_description == 'interface' and extends == ' extends java.lang.annotation.Annotation': 125 | kind_description = 'annotation' 126 | short_definition = '@%s' % signature 127 | 128 | modifier = match.group(1) 129 | kind_description = '%s%s' % (modifier, kind_description) 130 | 131 | return (Kind.KLASS, signature, short_definition, kind_description) 132 | 133 | # Constructor 134 | match = re.match('%s(%s)(\S+\(.*\))%s' % (visibility, modifiers, throws), full_definition) 135 | if match: 136 | kind_description = 'constructor' 137 | signature = short_definition = match.group(2) 138 | 139 | modifier = match.group(1) 140 | kind_description = '%s%s' % (modifier, kind_description) 141 | 142 | return (Kind.CONSTRUCTOR, signature, short_definition, kind_description) 143 | 144 | # Method 145 | # public static > void bind(T) throws java.lang.Exception; 146 | match = re.match('%s(%s)(%s)(%s)(\S+\(.*\))%s' % (visibility, modifiers, typed_parameter, object_type, throws), full_definition) 147 | if match: 148 | kind_description = 'method' 149 | type_param = match.group(2) 150 | return_type = match.group(3) 151 | name_and_params = match.group(4) 152 | 153 | signature = '%s%s' % (type_param, name_and_params) 154 | short_definition = '%s%s%s' % (type_param, return_type, name_and_params) 155 | 156 | modifier = match.group(1) 157 | if klass_definition.kind_description in ('interface', 'annotation'): 158 | modifier = re.sub('abstract\s+', '', modifier) 159 | kind_description = '%s%s' % (modifier, kind_description) 160 | 161 | return (Kind.METHOD, signature, short_definition, kind_description) 162 | 163 | # Field 164 | match = re.match('%s(%s)(%s(\S+))' % (visibility, modifiers, object_type), full_definition) 165 | if match: 166 | kind_description = 'field' 167 | signature = match.group(3) 168 | short_definition = match.group(2) 169 | 170 | modifier = match.group(1) 171 | kind_description = '%s%s' % (modifier, kind_description) 172 | 173 | return (Kind.FIELD, signature, short_definition, kind_description) 174 | 175 | raise Exception('Could not parse %s' % full_definition) 176 | 177 | 178 | def _changes(old_symbols, new_symbols): 179 | '''Groups the two maps of symbols by additions, deletions, and modifications, and returns a map of klass to changes.''' 180 | 181 | changes = defaultdict(list) # {Klass: [Modification, Modification, ...], ...} 182 | 183 | old = set(old_symbols.keys()) 184 | new = set(new_symbols.keys()) 185 | 186 | added = [symbol_id for symbol_id in new if symbol_id not in old] 187 | deleted = [symbol_id for symbol_id in old if symbol_id not in new] 188 | persisted = [symbol_id for symbol_id in old if symbol_id in new] 189 | 190 | # Additions 191 | for symbol_id in sorted(added): 192 | definition = new_symbols[symbol_id] 193 | changes[symbol_id.klass].append(Addition(symbol_id, definition)) 194 | 195 | # Deletions 196 | for symbol_id in sorted(deleted): 197 | definition = old_symbols[symbol_id] 198 | changes[symbol_id.klass].append(Deletion(symbol_id, definition)) 199 | 200 | # Modifications 201 | for symbol_id in sorted(persisted): 202 | old_definition = old_symbols[symbol_id] 203 | new_definition = new_symbols[symbol_id] 204 | if old_definition.full != new_definition.full: 205 | changes[symbol_id.klass].append(Modification(symbol_id, old_definition, new_definition)) 206 | 207 | return changes 208 | 209 | 210 | def _print_markdown(report): 211 | '''Prints the report in markdown format.''' 212 | 213 | for (klass, changes) in sorted(report.iteritems()): 214 | print('## %s' % _simplify(klass)) 215 | print('') 216 | print('\n\n'.join([_markdown_for_change(change) for change in changes])) 217 | print('') 218 | print('') 219 | 220 | 221 | def _markdown_for_change(change): 222 | if isinstance(change, Addition): 223 | return '*new* %s: %s' % (_markdown_for_kind(change), _markdown_for_short_definition(change)) 224 | elif isinstance(change, Deletion): 225 | return '*removed* %s: %s' % (_markdown_for_kind(change), _markdown_for_short_definition(change)) 226 | elif isinstance(change, Modification): 227 | return '\n'.join([ 228 | '*modified* %s: %s' % (_markdown_for_kind(change), _markdown_for_short_definition(change)), 229 | '', 230 | '| From: | %s |' % _markdown_for_old_full_definition(change), 231 | '| To: | %s |' % _markdown_for_new_full_definition(change) 232 | ]) 233 | raise Exception('Could not produce markdown for %s' % change) 234 | 235 | 236 | def _definition(change): 237 | if isinstance(change, Addition): 238 | return change.definition 239 | elif isinstance(change, Deletion): 240 | return change.definition 241 | elif isinstance(change, Modification): 242 | return change.old_definition 243 | raise Exception('Could not produce definition for %s' % change) 244 | 245 | 246 | def _markdown_for_kind(change): 247 | return _definition(change).kind_description 248 | 249 | 250 | def _markdown_for_short_definition(change): 251 | return '`%s`' % _simplify(_definition(change).short) 252 | 253 | 254 | def _markdown_for_old_full_definition(change): 255 | return _simplify(change.old_definition.full) 256 | 257 | 258 | def _markdown_for_new_full_definition(change): 259 | return _simplify(change.new_definition.full) 260 | 261 | 262 | def _simplify(string): 263 | '''Replaces all full class names with simple class names. 264 | 265 | Input: 266 | public com.google.android.material.motion.runtime.Performer$PerformerInstantiationException(java.lang.Class, java.lang.Exception) 267 | 268 | Returns: 269 | public PerformerInstantiationException(Class, Exception) 270 | ''' 271 | 272 | boundaries = r'(\(|\)|\s|<|>|,|@)' 273 | return ''.join([_simplify_token(token) for token in re.split(boundaries, string)]) 274 | 275 | 276 | def _simplify_token(token): 277 | delimiters = '.', '$' 278 | for delimiter in delimiters: 279 | token = token[token.rfind(delimiter)+1:] 280 | return token 281 | 282 | 283 | if __name__ == '__main__': 284 | if len(sys.argv) != 4: 285 | print('Usage: %s ' % sys.argv[0]) 286 | sys.exit(1) 287 | print_api_diff(sys.argv[1], sys.argv[2], sys.argv[3]) 288 | -------------------------------------------------------------------------------- /src/apidiff: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # 3 | # Copyright 2016-present The Material Motion Authors. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | usage() { 18 | echo "apidiff " 19 | echo 20 | echo "Engine-specific invocations:" 21 | echo 22 | echo " apidiff objc " 23 | echo " apidiff swift " 24 | echo " apidiff android " 25 | } 26 | 27 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 28 | ROOT=$(cd "$DIR/.." && pwd) 29 | 30 | OLD_COMMIT="$1" 31 | NEW_COMMIT="$2" 32 | ENGINE="$3" 33 | 34 | if [[ -z "$OLD_COMMIT" || -z "$NEW_COMMIT" || -z "$ENGINE" ]]; then 35 | usage 36 | exit 1 37 | fi 38 | 39 | OLD_REF=$(git rev-list -n 1 "$OLD_COMMIT" -- 2> /dev/null || echo "") 40 | if [ -z "$OLD_REF" ]; then 41 | echo "$OLD_COMMIT is not a known ref." 42 | exit 1 43 | fi 44 | 45 | NEW_REF=$(git rev-list -n 1 "$NEW_COMMIT" -- 2> /dev/null || echo "") 46 | if [ -z "$NEW_REF" ]; then 47 | echo "$NEW_COMMIT is not a known ref." 48 | exit 1 49 | fi 50 | 51 | TMP_PATH=$(mktemp -d) 52 | ERROR_LOG="$TMP_PATH/error.log" 53 | OLD_PATH="$TMP_PATH/old" 54 | NEW_PATH="$TMP_PATH/new" 55 | 56 | GIT_ROOT="$(git rev-parse --show-toplevel | tail -n1)" 57 | 58 | make_clones() { 59 | git clone "$GIT_ROOT" "$OLD_PATH" -q 2>> "$ERROR_LOG" || { echo "ERROR: Failed to clone."; exit 1; } 60 | pushd "$OLD_PATH" >> /dev/null 61 | git fetch -q || { echo "ERROR: Failed to update repo."; exit 1; } 62 | git checkout "$OLD_REF" -q 63 | popd >> /dev/null 64 | 65 | git clone "$GIT_ROOT" "$NEW_PATH" -q 2>> "$ERROR_LOG" || { echo "ERROR: Failed to clone."; exit 1; } 66 | pushd "$NEW_PATH" >> /dev/null 67 | git fetch -q || { echo "ERROR: Failed to update repo."; exit 1; } 68 | git checkout "$NEW_REF" -q 69 | popd >> /dev/null 70 | } 71 | 72 | clean_clones() { 73 | rm -rf "$OLD_PATH" 74 | rm -rf "$NEW_PATH" 75 | } 76 | 77 | make_clones 78 | 79 | echo "Auto-generated by running:" 80 | echo 81 | echo " apidiff $@" 82 | echo 83 | 84 | "$DIR/pathapidiff" "$OLD_PATH" "$NEW_PATH" "$ENGINE" "${@:4}" 85 | 86 | clean_clones 87 | -------------------------------------------------------------------------------- /src/pathapidiff: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # 3 | # Copyright 2016-present The Material Motion Authors. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | usage() { 18 | echo "pathapidiff " 19 | echo 20 | echo "Engine-specific invocations:" 21 | echo 22 | echo " pathapidiff objc " 23 | echo " pathapidiff swift " 24 | echo " pathapidiff android " 25 | } 26 | 27 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 28 | ROOT=$(cd "$DIR/.." && pwd) 29 | 30 | OLD_PATH="$1" 31 | NEW_PATH="$2" 32 | ENGINE="$3" 33 | 34 | if [[ -z "$OLD_PATH" || -z "$NEW_PATH" || -z "$ENGINE" ]]; then 35 | usage 36 | exit 1 37 | fi 38 | 39 | if [ ! -d "$OLD_PATH" ]; then 40 | echo "$OLD_PATH is not a valid folder." 41 | exit 1 42 | fi 43 | 44 | if [ ! -d "$NEW_PATH" ]; then 45 | echo "$NEW_PATH is not a valid folder." 46 | exit 1 47 | fi 48 | 49 | TMP_PATH=$(mktemp -d) 50 | ERROR_LOG="$TMP_PATH/error.log" 51 | 52 | apidiff_objc() { 53 | umbrella_header="$1" 54 | 55 | if [[ -z "$umbrella_header" ]]; then 56 | usage 57 | exit 1 58 | fi 59 | 60 | escaped_old_path=$(echo "$OLD_PATH" | sed "s:/:\\\\\\\\/:g") 61 | escaped_new_path=$(echo "$NEW_PATH" | sed "s:/:\\\\\\\\/:g") 62 | 63 | cd "$ROOT" 64 | 65 | sourcekitten doc --objc "$OLD_PATH/$umbrella_header" -- -x objective-c -arch arm64 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -I $(pwd) 2>> "$ERROR_LOG" | sed "s:$escaped_old_path\\\\/::g" > "$TMP_PATH/old_apis" 66 | sourcekitten doc --objc "$NEW_PATH/$umbrella_header" -- -x objective-c -arch arm64 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -I $(pwd) 2>> "$ERROR_LOG" | sed "s:$escaped_new_path\\\\/::g" > "$TMP_PATH/new_apis" 67 | 68 | pushd apple/diffreport > /dev/null 69 | swift build > /dev/null 2>> "$ERROR_LOG" 70 | swift run diffreport "$TMP_PATH/old_apis" "$TMP_PATH/new_apis" 2>> "$ERROR_LOG" 71 | popd > /dev/null 72 | } 73 | 74 | apidiff_swift() { 75 | workspace_path="$1" 76 | module_name="$2" 77 | 78 | if [[ -z "$workspace_path" || -z "$module_name" ]]; then 79 | usage 80 | exit 1 81 | fi 82 | 83 | escaped_old_path=$(echo "$OLD_PATH" | sed "s:/:\\\\\\\\/:g") 84 | escaped_new_path=$(echo "$NEW_PATH" | sed "s:/:\\\\\\\\/:g") 85 | 86 | cd "$ROOT" 87 | 88 | if [ -f "$OLD_PATH/Podfile" ]; then 89 | pushd "$OLD_PATH" >> /dev/null 90 | pod install >> /dev/null 2> /dev/null 91 | popd >> /dev/null 92 | fi 93 | 94 | if [ -d "$OLD_PATH/$workspace_path" ]; then 95 | scheme_name=$(xcodebuild build -workspace "$OLD_PATH/$workspace_path" -list | sed -e 's/^[[:space:]]*//' | grep -x "$module_name") 96 | 97 | if [[ -z "$scheme_name" ]]; then 98 | echo "Couldn't find scheme $module_name. Here's what we found:" 99 | xcodebuild build -workspace "$OLD_PATH/$workspace_path" -list 100 | echo "Filtering down gave us this list:" 101 | xcodebuild build -workspace "$OLD_PATH/$workspace_path" -list | sed -e 's/^[[:space:]]*//' | grep -x "$module_name" 102 | exit 1 103 | fi 104 | 105 | sourcekitten doc --module-name "$module_name" -- -workspace "$OLD_PATH/$workspace_path" -scheme ""$scheme_name"" 2>> "$ERROR_LOG" | sed "s:$escaped_old_path\\\\/::g" > "$TMP_PATH/old_apis" 106 | else 107 | touch "$TMP_PATH/old_apis" 108 | fi 109 | 110 | if [ -f "$NEW_PATH/Podfile" ]; then 111 | pushd "$NEW_PATH" >> /dev/null 112 | pod install >> /dev/null 2> /dev/null 113 | popd >> /dev/null 114 | fi 115 | 116 | scheme_name=$(xcodebuild build -workspace "$NEW_PATH/$workspace_path" -list | sed -e 's/^[[:space:]]*//' | grep -x "$module_name") 117 | 118 | if [[ -z "$scheme_name" ]]; then 119 | echo "Couldn't find scheme $module_name. Here's what we found:" 120 | xcodebuild build -workspace "$NEW_PATH/$workspace_path" -list 121 | echo "Filtering down gave us this list:" 122 | xcodebuild build -workspace "$NEW_PATH/$workspace_path" -list | sed -e 's/^[[:space:]]*//' | grep -x "$module_name" 123 | exit 1 124 | fi 125 | 126 | sourcekitten doc --module-name "$module_name" -- -workspace "$NEW_PATH/$workspace_path" -scheme ""$scheme_name"" 2>> "$ERROR_LOG" | sed "s:$escaped_new_path\\\\/::g" > "$TMP_PATH/new_apis" 127 | 128 | pushd apple/diffreport > /dev/null 129 | swift build > /dev/null 2>> "$ERROR_LOG" 130 | swift run diffreport "$TMP_PATH/old_apis" "$TMP_PATH/new_apis" 2>> "$ERROR_LOG" 131 | popd > /dev/null 132 | } 133 | 134 | apidiff_java() { 135 | library="$1" 136 | 137 | if [[ -z "$library" ]]; then 138 | usage 139 | exit 1 140 | fi 141 | 142 | case "$ENGINE" in 143 | android) artifact="$library/build/outputs/aar/$library-debug.aar" ;; 144 | java) artifact="$library/build/libs/$library.jar" ;; 145 | *) exit 1 ;; 146 | esac 147 | 148 | pushd "$OLD_PATH" >> /dev/null 149 | ./gradlew ":$library:build" &>/dev/null || { echo "ERROR: Failed to build $OLD_COMMIT."; exit 1; } 150 | old_artifact="$OLD_PATH/$artifact" 151 | popd >> /dev/null 152 | 153 | pushd "$NEW_PATH" >> /dev/null 154 | ./gradlew ":$library:build" &>/dev/null || { echo "ERROR: Failed to build $NEW_COMMIT."; exit 1; } 155 | new_artifact="$NEW_PATH/$artifact" 156 | popd >> /dev/null 157 | 158 | "$ROOT/third_party/jardiff/jardiff.py" "$old_artifact" "$new_artifact" "$ROOT/java/markdowndiff.py" 159 | } 160 | 161 | case "$ENGINE" in 162 | objc) apidiff_objc "${@:4}" ;; 163 | swift) apidiff_swift "${@:4}" ;; 164 | android|java) apidiff_java "${@:4}" ;; 165 | *) usage ;; 166 | esac 167 | 168 | if [ -s "$ERROR_LOG" ]; then 169 | >&2 echo "stderr output is available in $ERROR_LOG" 170 | fi 171 | --------------------------------------------------------------------------------