diff --git a/src/SwiftAPIViewCore.xctestplan b/src/SwiftAPIViewCore.xctestplan
new file mode 100644
index 00000000000..711a1c46314
--- /dev/null
+++ b/src/SwiftAPIViewCore.xctestplan
@@ -0,0 +1,24 @@
+{
+ "configurations" : [
+ {
+ "id" : "48782905-B2E9-45DE-B537-392AF13BD2AC",
+ "name" : "Test Scheme Action",
+ "options" : {
+
+ }
+ }
+ ],
+ "defaultOptions" : {
+
+ },
+ "testTargets" : [
+ {
+ "target" : {
+ "containerPath" : "container:SwiftAPIViewCore.xcodeproj",
+ "identifier" : "0A8469E827879AE200C967A8",
+ "name" : "SwiftAPIViewCoreTests"
+ }
+ }
+ ],
+ "version" : 1
+}
diff --git a/src/swift/CHANGELOG.md b/src/swift/CHANGELOG.md
index 32a92edae82..0d2b53a175f 100644
--- a/src/swift/CHANGELOG.md
+++ b/src/swift/CHANGELOG.md
@@ -1,5 +1,8 @@
# Release History
+## Version 0.3.0 (Unreleased)
+- Convert Swift APIView to use the new tree-token syntax.
+
## Version 0.2.2 (Unreleased)
- Fix issue where extension members were duplicated.
diff --git a/src/swift/SwiftAPIView.xcworkspace/contents.xcworkspacedata b/src/swift/SwiftAPIView.xcworkspace/contents.xcworkspacedata
index a80b0700a9c..f68d995b497 100644
--- a/src/swift/SwiftAPIView.xcworkspace/contents.xcworkspacedata
+++ b/src/swift/SwiftAPIView.xcworkspace/contents.xcworkspacedata
@@ -16,4 +16,7 @@
+
+
diff --git a/src/swift/SwiftAPIView/SwiftAPIView.xcodeproj/project.pbxproj b/src/swift/SwiftAPIView/SwiftAPIView.xcodeproj/project.pbxproj
index b9e62f3d4f7..97e4cf44b16 100644
--- a/src/swift/SwiftAPIView/SwiftAPIView.xcodeproj/project.pbxproj
+++ b/src/swift/SwiftAPIView/SwiftAPIView.xcodeproj/project.pbxproj
@@ -291,7 +291,7 @@
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = "$(SRCROOT)/Info.plist";
MACOSX_DEPLOYMENT_TARGET = 12.0;
- MARKETING_VERSION = 0.2.2;
+ MARKETING_VERSION = 0.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.SwiftAPIView;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
@@ -307,7 +307,7 @@
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = "$(SRCROOT)/Info.plist";
MACOSX_DEPLOYMENT_TARGET = 12.0;
- MARKETING_VERSION = 0.2.2;
+ MARKETING_VERSION = 0.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.SwiftAPIView;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
diff --git a/src/swift/SwiftAPIViewCore/Sources/APIViewManager.swift b/src/swift/SwiftAPIViewCore/Sources/APIViewManager.swift
index b480d75962c..a29874310bc 100644
--- a/src/swift/SwiftAPIViewCore/Sources/APIViewManager.swift
+++ b/src/swift/SwiftAPIViewCore/Sources/APIViewManager.swift
@@ -66,12 +66,15 @@ public class APIViewManager: SyntaxVisitor {
var mode: APIViewManagerMode
+ var model: CodeModel?
+
var statements = OrderedDictionary()
// MARK: Initializer
public init(mode: APIViewManagerMode = .commandLine) {
self.mode = mode
+ self.model = nil
super.init(viewMode: .all)
}
@@ -84,19 +87,18 @@ public class APIViewManager: SyntaxVisitor {
guard let sourceUrl = URL(string: config.sourcePath) else {
SharedLogger.fail("usage error: source path was invalid.")
}
- let apiView = try createApiView(from: sourceUrl)
-
+ model = try createApiView(from: sourceUrl)
switch mode {
case .commandLine:
- save(apiView: apiView)
+ save(apiView: model!)
return ""
case .testing:
- return apiView.text
+ return model!.text
}
}
/// Persist the token file to disk
- func save(apiView: APIViewModel) {
+ func save(apiView: CodeModel) {
let destUrl: URL
if let destPath = config.destPath {
destUrl = URL(fileURLWithPath: destPath)
@@ -189,7 +191,7 @@ public class APIViewManager: SyntaxVisitor {
return filePaths
}
- func createApiView(from sourceUrl: URL) throws -> APIViewModel {
+ func createApiView(from sourceUrl: URL) throws -> CodeModel {
SharedLogger.debug("URL: \(sourceUrl.absoluteString)")
var packageName: String?
var packageVersion: String?
@@ -232,8 +234,7 @@ public class APIViewManager: SyntaxVisitor {
}
config.packageName = packageName!
config.packageVersion = packageVersion!
- let apiViewName = "\(packageName!) (version \(packageVersion!))"
- let apiView = APIViewModel(name: apiViewName, packageName: packageName!, versionString: packageVersion!, statements: Array(statements.values))
+ let apiView = CodeModel(packageName: packageName!, packageVersion: packageVersion!, statements: Array(statements.values))
return apiView
}
diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift
deleted file mode 100644
index d07d37fb276..00000000000
--- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift
+++ /dev/null
@@ -1,380 +0,0 @@
-// --------------------------------------------------------------------------
-//
-// Copyright (c) Microsoft Corporation. All rights reserved.
-//
-// The MIT License (MIT)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the ""Software""), to
-// deal in the Software without restriction, including without limitation the
-// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-// sell copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-// IN THE SOFTWARE.
-//
-// --------------------------------------------------------------------------
-
-import Foundation
-import SwiftSyntax
-
-
-class APIViewModel: Tokenizable, Encodable {
-
- /// The name to be used in the APIView review list
- var name: String
-
- /// The package name used by APIView
- var packageName: String
-
- /// The version string
- var versionString: String
-
- /// Language string.
- let language = "Swift"
-
- /// The APIView tokens to display
- var tokens: [Token]
-
- /// The navigation tokens to display
- var navigation: [NavigationToken]
-
- /// Node-based representation of the Swift package
- var model: PackageModel
-
- /// Current indentation level
- private var indentLevel = 0
-
- /// Whether indentation is needed
- private var needsIndent = false
-
- /// Number of spaces to indent per level
- let indentSpaces = 4
-
- /// Access modifier to expose via APIView
- static let publicModifiers: [AccessLevel] = [.public, .open]
-
- /// sentinel value for unresolved type references
- static let unresolved = "__UNRESOLVED__"
-
- /// Tracks assigned definition IDs so they can be linked
- private var definitionIds = Set()
-
- /// Returns the text-based representation of all tokens
- var text: String {
- return tokens.map { $0.text }.joined()
- }
-
- // MARK: Initializers
-
- init(name: String, packageName: String, versionString: String, statements: [CodeBlockItemSyntax.Item]) {
- self.name = name
- self.versionString = versionString
- self.packageName = packageName
- navigation = [NavigationToken]()
- tokens = [Token]()
- model = PackageModel(name: packageName, statements: statements)
- self.tokenize(apiview: self, parent: nil)
- model.navigationTokenize(apiview: self, parent: nil)
- }
-
- // MARK: Codable
-
- enum CodingKeys: String, CodingKey {
- case name = "Name"
- case tokens = "Tokens"
- case language = "Language"
- case packageName = "PackageName"
- case navigation = "Navigation"
- case versionString = "VersionString"
- }
-
- func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
- try container.encode(name, forKey: .name)
- try container.encode(packageName, forKey: .packageName)
- try container.encode(language, forKey: .language)
- try container.encode(tokens, forKey: .tokens)
- try container.encode(navigation, forKey: .navigation)
- try container.encode(versionString, forKey: .versionString)
- }
-
- func tokenize(apiview a: APIViewModel, parent: Linkable?) {
- // Renders the APIView "preamble"
- let bundle = Bundle(for: Swift.type(of: self))
- let versionKey = "CFBundleShortVersionString"
- let apiViewVersion = bundle.object(forInfoDictionaryKey: versionKey) as? String ?? "Unknown"
- a.text("Package parsed using Swift APIView (version \(apiViewVersion))")
- a.newline()
- a.blankLines(set: 2)
- model.tokenize(apiview: a, parent: parent)
- }
-
- // MARK: Token Emitters
- func add(token: Token) {
- self.tokens.append(token)
- }
-
- func add(token: NavigationToken) {
- self.navigation.append(token)
- }
-
- func text(_ text: String, definitionId: String? = nil) {
- checkIndent()
- let item = Token(definitionId: definitionId, navigateToId: nil, value: text, kind: .text)
- // TODO: Add cross-language definition ID
- // if add_cross_language_id:
- // token.cross_language_definition_id = self.metadata_map.cross_language_map.get(id, None)
- add(token: item)
- }
-
- /// Used to END a line and wrap to the next. Cannot be used to inject blank lines.
- func newline() {
- // strip trailing whitespace token, except blank lines
- if tokens.last?.kind == .whitespace {
- let popped = tokens.popLast()!
- // lines that consist only of whitespace must be preserved
- if tokens.last?.kind == .newline {
- add(token: popped)
- }
- }
- checkIndent()
- // don't add newline if one already in place
- if tokens.last?.kind != .newline {
- let item = Token(definitionId: nil, navigateToId: nil, value: nil, kind: .newline)
- add(token: item)
- }
- needsIndent = true
- }
-
- /// Ensures a specific number of blank lines. Will add or remove newline
- /// tokens as needed to ensure the exact number of blank lines.
- func blankLines(set count: Int) {
- // count the number of trailing newlines
- var newlineCount = 0
- for token in self.tokens.reversed() {
- if token.kind == .newline {
- newlineCount += 1
- } else {
- break
- }
- }
- if newlineCount < (count + 1) {
- // if not enough newlines, add some
- let linesToAdd = (count + 1) - newlineCount
- for _ in 0.. (count + 1) {
- // if there are too many newlines, remove some
- let linesToRemove = newlineCount - (count + 1)
- for _ in 0.. DeclarationModel? {
- guard let item = item else { return nil }
- switch item.kind {
- case .method:
- // look to the parent of the function
- return findNonFunctionParent(from: (item.parent as? DeclarationModel))
- default:
- return item
- }
- }
-
- /// Link to a registered type
- func typeReference(name: String, parent: DeclarationModel?) {
- checkIndent()
- if name == "IncomingAudioStream" {
- let test = "best"
- }
- let linkId = definitionId(for: name, withParent: parent) ?? APIViewModel.unresolved
- let item = Token(definitionId: nil, navigateToId: linkId, value: name, kind: .typeName)
- add(token: item)
- }
-
- func definitionId(for val: String, withParent parent: DeclarationModel?) -> String? {
- var matchVal = val
- if !matchVal.contains("."), let parentObject = findNonFunctionParent(from: parent), let parentDefId = parentObject.definitionId {
- // if a plain, undotted name is provided, try to append the parent prefix
- matchVal = "\(parentDefId).\(matchVal)"
- }
- let matches: [String]
- if matchVal.contains(".") {
- matches = definitionIds.filter { $0.hasSuffix(matchVal) }
- } else {
- // if type does not contain a dot, then suffix is insufficient
- // we must completely match the final segment of the type name
- matches = definitionIds.filter { $0.split(separator: ".").last! == matchVal }
- }
- if matches.count > 1 {
- SharedLogger.warn("Found \(matches.count) matches for \(matchVal). Using \(matches.first!). Swift APIView may not link correctly.")
- }
- return matches.first
- }
-
- func member(name: String, definitionId: String? = nil) {
- checkIndent()
- let item = Token(definitionId: definitionId, navigateToId: nil, value: name, kind: .memberName)
- add(token: item)
- }
-
- // TODO: Add support for diagnostics
-// func diagnostic(self, text, line_id):
-// self.diagnostics.append(Diagnostic(line_id, text))
-
- func comment(_ text: String) {
- checkIndent()
- var message = text
- if !text.starts(with: "\\") {
- message = "\\\\ \(message)"
- }
- let item = Token(definitionId: nil, navigateToId: nil, value: message, kind: .comment)
- add(token: item)
- }
-
- func literal(_ value: String) {
- let item = Token(definitionId: nil, navigateToId: nil, value: value, kind: .literal)
- add(token: item)
- }
-
- func stringLiteral(_ text: String) {
- let item = Token(definitionId: nil, navigateToId: nil, value: "\"\(text)\"", kind: .stringLiteral)
- add(token: item)
- }
-
- /// Wraps code in an indentation
- func indent(_ indentedCode: () -> Void) {
- indentLevel += 1
- indentedCode()
- // Don't end an indentation block with blank lines
- let tokenSuffix = Array(tokens.suffix(2))
- if tokenSuffix.count == 2 && tokenSuffix[0].kind == .newline && tokenSuffix[1].kind == .newline {
- _ = tokens.popLast()
- }
- indentLevel -= 1
- }
-
- /// Checks if indentation is needed and adds whitespace as needed
- func checkIndent() {
- guard needsIndent else { return }
- whitespace(count: indentLevel * indentSpaces)
- needsIndent = false
- }
-
- /// Constructs a definition ID and ensures it is unique.
- func defId(forName name: String, withPrefix prefix: String?) -> String {
- var defId = prefix != nil ? "\(prefix!).\(name)" : name
- defId = defId.filter { !$0.isWhitespace }
- if self.definitionIds.contains(defId) {
- SharedLogger.warn("Duplicate definition ID: \(defId). APIView will display duplicate comments.")
- }
- definitionIds.insert(defId)
- return defId
- }
-
- /// Trims whitespace tokens
- func trim(removeNewlines: Bool = false) {
- var lineIds = [Token]()
- while (!tokens.isEmpty) {
- var continueTrim = true
- if let kind = tokens.last?.kind {
- switch kind {
- case .whitespace:
- _ = tokens.popLast()
- case .newline:
- if removeNewlines {
- _ = tokens.popLast()
- } else {
- continueTrim = false
- }
- case .lineIdMarker:
- if let popped = tokens.popLast() {
- lineIds.append(popped)
- }
- default:
- continueTrim = false
- }
- }
- if !continueTrim {
- break
- }
- }
- // reappend the line id tokens
- while (!lineIds.isEmpty) {
- if let popped = lineIds.popLast() {
- tokens.append(popped)
- }
- }
- }
-}
diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/CodeDiagnostic.swift b/src/swift/SwiftAPIViewCore/Sources/Models/CodeDiagnostic.swift
new file mode 100644
index 00000000000..88eccda6c30
--- /dev/null
+++ b/src/swift/SwiftAPIViewCore/Sources/Models/CodeDiagnostic.swift
@@ -0,0 +1,82 @@
+// --------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+// The MIT License (MIT)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the ""Software""), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//
+// --------------------------------------------------------------------------
+
+import Foundation
+import SwiftSyntax
+
+enum CodeDiagnosticLevel: Int {
+ case Info = 1
+ case Warning = 2
+ case Error = 3
+ /// Fatal level diagnostic will block API review approval and it will show an error message to the user. Approver will have to
+ /// override fatal level system comments before approving a review.
+ case Fatal = 4
+}
+
+class CodeDiagnostic: Tokenizable, Encodable {
+ /// The diagnostic ID...?
+ var diagnosticId: String?
+ /// Id of ReviewLine object where this diagnostic needs to be displayed
+ var targetId: String
+ /// Auto generated system comment to be displayed under targeted line.
+ var text: String
+ var level: CodeDiagnosticLevel
+ var helpLinkUri: String?
+
+ init(_ text: String, targetId: String, level: CodeDiagnosticLevel, helpLink: String?) {
+ self.text = text
+ self.targetId = targetId
+ self.level = level
+ self.helpLinkUri = helpLink
+ // FIXME: What is this for?
+ self.diagnosticId = nil
+ }
+
+ // MARK: Codable
+
+ enum CodingKeys: String, CodingKey {
+ case diagnosticId = "DiagnosticId"
+ case targetId = "TargetId"
+ case text = "Text"
+ case level = "Level"
+ case helpLinkUri = "HelpLinkUri"
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(targetId, forKey: .targetId)
+ try container.encode(text, forKey: .text)
+ try container.encode(level.rawValue, forKey: .level)
+ try container.encodeIfPresent(helpLinkUri, forKey: .helpLinkUri)
+ try container.encodeIfPresent(diagnosticId, forKey: .diagnosticId)
+ }
+
+ // MARK: Tokenizable
+
+ func tokenize(apiview: CodeModel, parent: (any Linkable)?) {
+ fatalError("Not implemented!")
+ }
+}
diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift
new file mode 100644
index 00000000000..dc595a63074
--- /dev/null
+++ b/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift
@@ -0,0 +1,396 @@
+// --------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+// The MIT License (MIT)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the ""Software""), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//
+// --------------------------------------------------------------------------
+
+import Foundation
+import SwiftSyntax
+
+
+class CodeModel: Tokenizable, Encodable {
+
+ /// The package name used by APIView
+ var packageName: String
+
+ /// The package version
+ var packageVersion: String
+
+ /// Version of the APIView language parser used to create the token file
+ let parserVersion: String
+
+ /// Language discriminator
+ let language = "Swift"
+
+ /// Only applicable currently to Java language variants
+ let languageVariant: String? = nil
+
+ /// The cross-language ID of the package
+ let crossLanguagePackageId: String?
+
+ /// The top level review lines for the APIView
+ var reviewLines: [ReviewLine]
+
+ /// System generated comments. Each is linked to a review line ID.
+ var diagnostics: [CodeDiagnostic]?
+
+ /// Node-based representation of the Swift package
+ private var model: PackageModel
+
+ /// Access modifier to expose via APIView
+ static let publicModifiers: [AccessLevel] = [.public, .open]
+
+ /// sentinel value for unresolved type references
+ static let unresolved = "__UNRESOLVED__"
+
+ /// sentinel value for missing IDs
+ static let missingId = "__MISSING__"
+
+ /// Tracks assigned definition IDs so they can be linked
+ private var definitionIds = Set()
+
+ /// Stores the current line. All helper methods append to this.
+ internal var currentLine: ReviewLine
+
+ /// Stores the parent of the current line.
+ internal var currentParent: ReviewLine? = nil
+
+ /// Stores the stack of parent lines
+ private var parentStack: [ReviewLine] = []
+
+ /// Used to track the currently processed namespace.
+ private var namespaceStack = NamespaceStack()
+
+ /// Returns the text-based representation of all lines
+ var text: String {
+ var value = ""
+ for line in reviewLines {
+ let lineText = line.text()
+ value += lineText
+ }
+ return value
+ }
+
+ // MARK: Initializers
+
+ init(packageName: String, packageVersion: String, statements: [CodeBlockItemSyntax.Item]) {
+ // Renders the APIView "preamble"
+ let bundle = Bundle(for: Swift.type(of: self))
+ let versionKey = "CFBundleShortVersionString"
+ self.packageVersion = packageVersion
+ self.packageName = packageName
+ self.parserVersion = bundle.object(forInfoDictionaryKey: versionKey) as? String ?? "Unknown"
+ self.currentLine = ReviewLine()
+ reviewLines = [ReviewLine]()
+ diagnostics = [CodeDiagnostic]()
+ model = PackageModel(name: packageName, statements: statements)
+ // FIXME: Actually wire this up!
+ self.crossLanguagePackageId = nil
+ self.tokenize(apiview: self, parent: nil)
+ }
+
+ // MARK: Codable
+
+ enum CodingKeys: String, CodingKey {
+ case packageName = "PackageName"
+ case packageVersion = "PackageVersion"
+ case parserVersion = "ParserVersion"
+ case language = "Language"
+ case languageVariant = "LanguageVariant"
+ case crossLanguagePackageId = "CrossLanguagePackageId"
+ case reviewLines = "ReviewLines"
+ case diagnostics = "Diagnostics"
+ case navigation = "Navigation"
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(packageName, forKey: .packageName)
+ try container.encode(packageVersion, forKey: .packageVersion)
+ try container.encode(parserVersion, forKey: .parserVersion)
+ try container.encode(language, forKey: .language)
+ try container.encode(languageVariant, forKey: .languageVariant)
+ try container.encode(reviewLines, forKey: .reviewLines)
+ try container.encodeIfPresent(crossLanguagePackageId, forKey: .crossLanguagePackageId)
+ try container.encodeIfPresent(diagnostics, forKey: .diagnostics)
+ }
+
+ func tokenize(apiview a: CodeModel, parent: Linkable?) {
+ self.text("Package parsed using Swift APIView (version \(self.parserVersion))")
+ self.newline()
+ self.blankLines(set: 2)
+ model.tokenize(apiview: a, parent: parent)
+ }
+
+ // MARK: Token Emitters
+ func token(kind: TokenKind, value: String, options: ReviewTokenOptions? = nil) {
+ var options = options ?? ReviewTokenOptions()
+ // workaround the silly server default
+ options.hasSuffixSpace = options.hasSuffixSpace != nil ? options.hasSuffixSpace : false
+ let token = ReviewToken(kind: kind, value: value, options: options)
+ self.currentLine.tokens.append(token)
+ return
+ }
+
+ func token(_ token: ReviewToken) {
+ self.currentLine.tokens.append(token)
+ return
+ }
+
+ func text(_ text: String, options: ReviewTokenOptions? = nil) {
+ self.token(kind: .text, value: text, options: options)
+ }
+
+
+ /// Ensure that `currentLine` has no leading or trailing space
+ func trimBeginningAndEnd() {
+ if let firstToken = currentLine.tokens.first {
+ firstToken.hasPrefixSpace = false
+ }
+
+ if let lastToken = currentLine.tokens.last {
+ lastToken.hasSuffixSpace = false
+ }
+ }
+
+ func newline() {
+ trimBeginningAndEnd()
+ if let currentParent = self.currentParent {
+ currentParent.children.append(self.currentLine)
+ } else {
+ self.reviewLines.append(self.currentLine)
+ }
+ self.currentLine = ReviewLine()
+ }
+
+ /// Set the exact number of desired newlines.
+ func blankLines(set count: Int) {
+ self.newline()
+ let parentLines = self.currentParent?.children ?? self.reviewLines
+ // count the number of trailing newlines
+ var newlineCount = 0
+ for line in parentLines.reversed() {
+ if line.tokens.count == 0 {
+ newlineCount += 1
+ } else {
+ break
+ }
+ }
+ if newlineCount == count {
+ return
+ } else if (newlineCount > count) {
+ // if there are too many newlines, remove some
+ let linesToRemove = newlineCount - count
+ for _ in 0.. 1 {
+ let token = ReviewToken(kind: .stringLiteral, value: "\u{0022}\(text)\u{0022}", options: options)
+ self.token(token)
+ } else {
+ self.punctuation("\u{0022}\u{0022}\u{0022}", options: options)
+ self.newline()
+ for line in lines {
+ self.literal(String(line), options: options)
+ self.newline()
+ }
+ self.punctuation("\u{0022}\u{0022}\u{0022}", options: options)
+ }
+ }
+
+ /// Wraps code in indentattion
+ func indent(_ indentedCode: () -> Void) {
+ trimBeginningAndEnd()
+ if let currentParent = self.currentParent {
+ currentParent.children.append(self.currentLine)
+ self.parentStack.append(currentParent)
+ } else {
+ self.reviewLines.append(self.currentLine)
+ }
+ self.currentParent = self.currentLine
+ self.currentLine = ReviewLine()
+
+ // handle the indented bodies
+ indentedCode()
+
+ // handle the de-indent logic
+ guard let currentParent = self.currentParent else {
+ fatalError("Cannot de-indent without a parent")
+ }
+ // ensure that the last line before the deindent has no blank lines
+ if let lastChild = currentParent.children.popLast() {
+ if (lastChild.tokens.count > 0) {
+ currentParent.children.append(lastChild)
+ }
+ }
+ self.currentParent = self.parentStack.popLast()
+ self.currentLine = ReviewLine()
+ }
+
+ /// Constructs a definition ID and ensures it is unique.
+ func defId(forName name: String, withPrefix prefix: String?) -> String {
+ var defId = prefix != nil ? "\(prefix!).\(name)" : name
+ defId = defId.filter { !$0.isWhitespace }
+ if self.definitionIds.contains(defId) {
+ SharedLogger.warn("Duplicate definition ID: \(defId). APIView will display duplicate comments.")
+ }
+ definitionIds.insert(defId)
+ return defId
+ }
+
+ /// Places the provided token in the tree based on the provided characters.
+ func snap(token target: ReviewToken, to characters: String) {
+ let allowed = Set(characters)
+ let lastLine = self.getLastLine() ?? self.currentLine
+
+ // iterate through tokens in reverse order
+ for token in lastLine.tokens.reversed() {
+ switch token.kind {
+ case .text:
+ // skip blank whitespace tokens
+ let value = token.value.trimmingTrailingCharacters(in: .whitespaces)
+ if value.count == 0 {
+ continue
+ } else {
+ // no snapping, so render in place
+ self.token(target)
+ return
+ }
+ case .punctuation:
+ // ensure no whitespace after the trim character
+ if allowed.first(where: { String($0) == token.value }) != nil {
+ token.hasSuffixSpace = false
+ target.hasSuffixSpace = false
+ lastLine.tokens.append(target)
+ return
+ } else {
+ // no snapping, so render in place
+ self.token(target)
+ return
+ }
+ default:
+ // no snapping, so render in place
+ self.token(target)
+ return
+ }
+ }
+ }
+
+ /// Retrieves the last line from the review
+ func getLastLine() -> ReviewLine? {
+ var current: ReviewLine? = nil
+ if let currentParent = self.currentParent {
+ current = currentParent.children.last
+ } else if let lastReviewLine = self.reviewLines.last {
+ current = lastReviewLine
+ }
+ if let lastChild = current?.children.last {
+ current = lastChild
+ }
+ return current
+ }
+}
diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift
index d83817d0098..7f55481eb6a 100644
--- a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift
+++ b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift
@@ -122,6 +122,7 @@ class DeclarationModel: Tokenizable, Linkable, Equatable {
/// Used for most declaration types that have members
convenience init(from decl: SyntaxProtocol, parent: Linkable?) {
let name = (decl as? hasIdentifier)!.identifier.withoutTrivia().text
+
let defId = identifier(forName: name, withPrefix: parent?.definitionId)
self.init(name: name, decl: decl, defId: defId, parent: parent, kind: .struct)
}
@@ -147,18 +148,29 @@ class DeclarationModel: Tokenizable, Linkable, Equatable {
self.extensions = []
}
- func tokenize(apiview a: APIViewModel, parent: Linkable?) {
- for child in childNodes {
+ func tokenize(apiview a: CodeModel, parent: Linkable?) {
+ for (idx, child) in childNodes.enumerated() {
switch child.kind {
case .attributeList:
+ // Ensure declarations that have an attributeList have a blank line,
+ // unless it is the first child
+ a.blankLines(set: a.currentParent?.children.count ?? 0 == 0 ? 0 : 1)
// attributes on declarations should have newlines
let obj = AttributeListSyntax(child)!
let children = obj.children(viewMode: .sourceAccurate)
for attr in children {
let attrText = attr.withoutTrivia().description.filter { !$0.isWhitespace }
- a.lineIdMarker(definitionId: "\(definitionId!).\(attrText)")
+ a.lineMarker("\(definitionId!).\(attrText)")
+ a.currentLine.relatedToLine = definitionId
attr.tokenize(apiview: a, parent: parent)
- a.newline()
+ a.blankLines(set: 0)
+ }
+ case .precedenceGroupAttributeList:
+ let obj = PrecedenceGroupAttributeListSyntax(child)!
+ a.indent {
+ for item in obj.children(viewMode: .sourceAccurate) {
+ item.tokenize(apiview: a, parent: nil)
+ }
a.blankLines(set: 0)
}
case .token:
@@ -166,21 +178,23 @@ class DeclarationModel: Tokenizable, Linkable, Equatable {
// render as the correct APIView token type
switch self.kind {
case .class:
- a.typeDeclaration(name: name, definitionId: definitionId)
+ a.typeDeclaration(name: name, typeId: definitionId)
case .enum:
- a.typeDeclaration(name: name, definitionId: definitionId)
+ a.typeDeclaration(name: name, typeId: definitionId)
case .method:
- a.member(name: name, definitionId: definitionId)
+ a.lineMarker(definitionId)
+ a.member(name: name)
case .package:
- a.typeDeclaration(name: name, definitionId: definitionId)
+ a.typeDeclaration(name: name, typeId: definitionId)
case .protocol:
- a.typeDeclaration(name: name, definitionId: definitionId)
+ a.typeDeclaration(name: name, typeId: definitionId)
case .struct:
- a.typeDeclaration(name: name, definitionId: definitionId)
+ a.typeDeclaration(name: name, typeId: definitionId)
case .unknown:
- a.typeDeclaration(name: name, definitionId: definitionId)
+ a.typeDeclaration(name: name, typeId: definitionId)
case .variable:
- a.member(name: name, definitionId: definitionId)
+ a.lineMarker(definitionId)
+ a.member(name: name)
}
} else {
child.tokenize(apiview: a, parent: nil)
@@ -191,21 +205,12 @@ class DeclarationModel: Tokenizable, Linkable, Equatable {
}
}
if !extensions.isEmpty {
- a.blankLines(set: 1)
- for ext in extensions {
- ext.tokenize(apiview: a, parent: self)
- a.blankLines(set: 1)
- }
+ extensions.tokenize(apiview: a, parent: self)
}
}
- func navigationTokenize(apiview a: APIViewModel, parent: Linkable?) {
- let navigationId = parent != nil ? "\(parent!.name).\(name)" : name
- a.add(token: NavigationToken(name: name, navigationId: navigationId, typeKind: kind.navigationSymbol, members: []))
- }
-
func shouldShow() -> Bool {
- let publicModifiers = APIViewModel.publicModifiers
+ let publicModifiers = CodeModel.publicModifiers
guard let parentDecl = (parent as? DeclarationModel) else {
return publicModifiers.contains(self.accessLevel)
}
diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift
index 71943cc6ec9..76c83f36b46 100644
--- a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift
+++ b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift
@@ -28,13 +28,15 @@ import Foundation
import SwiftSyntax
-class ExtensionModel: Tokenizable {
+class ExtensionModel: Tokenizable, Linkable {
var accessLevel: AccessLevel
var extendedType: String
- var definitionId: String
+ var name: String
+ var definitionId: String?
+ // treat extensions as if they have no parents
+ var parent: Linkable? = nil
var members: [DeclarationModel]
let childNodes: SyntaxChildren
- var parent: Linkable?
private let decl: ExtensionDeclSyntax
/// Initialize from initializer declaration
@@ -53,8 +55,8 @@ class ExtensionModel: Tokenizable {
}
self.members = [DeclarationModel]()
self.decl = decl
- self.definitionId = ""
- self.parent = nil
+ self.definitionId = nil
+ self.name = ""
}
private func identifier(for decl: ExtensionDeclSyntax) -> String {
@@ -71,91 +73,93 @@ class ExtensionModel: Tokenizable {
return defId
}
- func processMembers(withParent parent: Linkable?) {
- self.parent = parent
+ func processMembers() {
self.definitionId = identifier(for: decl)
for member in decl.members.members {
let decl = member.decl
switch decl.kind {
case .actorDecl:
- appendIfVisible(DeclarationModel(from: ActorDeclSyntax(decl)!, parent: parent))
+ appendIfVisible(DeclarationModel(from: ActorDeclSyntax(decl)!, parent: self))
case .classDecl:
- appendIfVisible(DeclarationModel(from: ClassDeclSyntax(decl)!, parent: parent))
+ appendIfVisible(DeclarationModel(from: ClassDeclSyntax(decl)!, parent: self))
case .deinitializerDecl:
// deinitializers cannot be called by users, so it makes no sense
// to expose them in APIView
break
case .enumDecl:
- appendIfVisible(DeclarationModel(from: EnumDeclSyntax(decl)!, parent: parent))
+ appendIfVisible(DeclarationModel(from: EnumDeclSyntax(decl)!, parent: self))
case .functionDecl:
- appendIfVisible(DeclarationModel(from: FunctionDeclSyntax(decl)!, parent: parent))
+ appendIfVisible(DeclarationModel(from: FunctionDeclSyntax(decl)!, parent: self))
case .importDecl:
// purposely ignore import declarations
break
case .operatorDecl:
- let model = DeclarationModel(from: OperatorDeclSyntax(decl)!, parent: parent)
+ let model = DeclarationModel(from: OperatorDeclSyntax(decl)!, parent: self)
// operators are global and must always be displayed
model.accessLevel = .public
appendIfVisible(model)
case .precedenceGroupDecl:
- let model = DeclarationModel(from: PrecedenceGroupDeclSyntax(decl)!, parent: parent)
+ let model = DeclarationModel(from: PrecedenceGroupDeclSyntax(decl)!, parent: self)
// precedence groups are public and must always be displayed
model.accessLevel = .public
appendIfVisible(model)
case .protocolDecl:
- appendIfVisible(DeclarationModel(from: ProtocolDeclSyntax(decl)!, parent: parent))
+ appendIfVisible(DeclarationModel(from: ProtocolDeclSyntax(decl)!, parent: self))
case .structDecl:
- appendIfVisible(DeclarationModel(from: StructDeclSyntax(decl)!, parent: parent))
+ appendIfVisible(DeclarationModel(from: StructDeclSyntax(decl)!, parent: self))
case .typealiasDecl:
- appendIfVisible(DeclarationModel(from: TypealiasDeclSyntax(decl)!, parent: parent))
+ appendIfVisible(DeclarationModel(from: TypealiasDeclSyntax(decl)!, parent: self))
case .extensionDecl:
SharedLogger.warn("Extensions containing extensions is not supported. Contact the Swift APIView team.")
case .initializerDecl:
- appendIfVisible(DeclarationModel(from: InitializerDeclSyntax(decl)!, parent: parent))
+ appendIfVisible(DeclarationModel(from: InitializerDeclSyntax(decl)!, parent: self))
case .subscriptDecl:
- appendIfVisible(DeclarationModel(from: SubscriptDeclSyntax(decl)!, parent: parent))
+ appendIfVisible(DeclarationModel(from: SubscriptDeclSyntax(decl)!, parent: self))
case .variableDecl:
- appendIfVisible(DeclarationModel(from: VariableDeclSyntax(decl)!, parent: parent))
+ appendIfVisible(DeclarationModel(from: VariableDeclSyntax(decl)!, parent: self))
default:
// Create an generic declaration model of unknown type
- appendIfVisible(DeclarationModel(from: decl, parent: parent))
+ appendIfVisible(DeclarationModel(from: decl, parent: self))
}
}
}
func appendIfVisible(_ decl: DeclarationModel) {
- let publicModifiers = APIViewModel.publicModifiers
+ let publicModifiers = CodeModel.publicModifiers
if publicModifiers.contains(decl.accessLevel) || publicModifiers.contains(self.accessLevel) {
self.members.append(decl)
}
}
- func tokenize(apiview a: APIViewModel, parent: Linkable?) {
+ func tokenize(apiview a: CodeModel, parent: Linkable?) {
for child in childNodes {
+ var options = ReviewTokenOptions()
let childIdx = child.indexInParent
- if childIdx == 13 {
+ if childIdx == 7 {
+ child.tokenize(apiview: a, parent: parent)
+ if let last = a.currentLine.tokens.popLast() {
+ // These are made as type references, but they should be
+ // type declarations
+ a.currentLine.lineId = self.definitionId
+ last.navigateToId = self.definitionId
+ a.currentLine.tokens.append(last)
+ }
+ } else if childIdx == 13 {
// special case for extension members
- a.punctuation("{", spacing: SwiftSyntax.TokenKind.leftBrace.spacing)
+ options.applySpacing(SwiftSyntax.TokenKind.leftBrace.spacing)
+ a.punctuation("{", options: options)
if !members.isEmpty {
a.indent {
for member in members {
- a.newline()
member.tokenize(apiview: a, parent: parent)
+ a.blankLines(set: 0)
}
}
}
+ a.blankLines(set: 0)
+ options.applySpacing(SwiftSyntax.TokenKind.rightBrace.spacing)
+ a.punctuation("}", options: options)
a.newline()
- a.punctuation("}", spacing: SwiftSyntax.TokenKind.rightBrace.spacing)
- a.newline()
- } else if childIdx == 7 {
- child.tokenize(apiview: a, parent: parent)
- if var last = a.tokens.popLast() {
- // These are made as type references, but they should be
- // type declarations
- last.definitionId = self.definitionId
- last.navigateToId = self.definitionId
- a.tokens.append(last)
- }
} else {
child.tokenize(apiview: a, parent: parent)
}
@@ -164,10 +168,24 @@ class ExtensionModel: Tokenizable {
}
extension Array {
+ func tokenize(apiview a: CodeModel, parent: Linkable?) {
+ a.blankLines(set: 1)
+ let lastIdx = self.count - 1
+ for (idx, ext) in self.enumerated() {
+ ext.tokenize(apiview: a, parent: parent)
+ if idx != lastIdx {
+ a.blankLines(set: 1)
+ }
+ }
+ }
+
func resolveDuplicates() -> [ExtensionModel] {
var resolved = [String: ExtensionModel]()
for ext in self {
- if let match = resolved[ext.definitionId] {
+ guard let defId = ext.definitionId else {
+ fatalError("No definition ID found for extension!")
+ }
+ if let match = resolved[defId] {
let resolvedMembers = Dictionary(uniqueKeysWithValues: match.members.map { ($0.definitionId, $0) } )
for member in ext.members {
if resolvedMembers[member.definitionId] != nil {
@@ -177,9 +195,9 @@ extension Array {
}
}
} else {
- resolved[ext.definitionId] = ext
+ resolved[defId] = ext
}
}
- return Array(resolved.values).sorted(by: {$0.definitionId < $1.definitionId })
+ return Array(resolved.values).sorted(by: {$0.definitionId! < $1.definitionId! })
}
}
diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/NamespaceStack.swift b/src/swift/SwiftAPIViewCore/Sources/Models/NamespaceStack.swift
new file mode 100644
index 00000000000..0f597026da4
--- /dev/null
+++ b/src/swift/SwiftAPIViewCore/Sources/Models/NamespaceStack.swift
@@ -0,0 +1,53 @@
+// --------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+// The MIT License (MIT)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the ""Software""), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//
+// --------------------------------------------------------------------------
+
+import Foundation
+
+
+/// Simple structure to track namespaces
+public struct NamespaceStack {
+ private var stack: [String] = []
+
+ /// Push a namespace segment onto the stack
+ mutating func push(_ val: String) {
+ self.stack.append(val)
+ }
+
+ /// Remove the last namespace segment from the stack
+ mutating func pop() -> String? {
+ return self.stack.popLast()
+ }
+
+ /// Get the fully qualified namespace
+ func value() -> String {
+ return self.stack.joined(separator: ".")
+ }
+
+ /// Reset the namespace stack to empty
+ mutating func reset() {
+ self.stack = []
+ }
+}
diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/NavigationToken.swift b/src/swift/SwiftAPIViewCore/Sources/Models/NavigationToken.swift
index f56dd270d3b..899afc3a556 100644
--- a/src/swift/SwiftAPIViewCore/Sources/Models/NavigationToken.swift
+++ b/src/swift/SwiftAPIViewCore/Sources/Models/NavigationToken.swift
@@ -55,7 +55,7 @@ class NavigationToken: Codable {
self.navigationId = navigationId
tags = NavigationTags(typeKind: typeKind)
for ext in extensions {
- let extNav = NavigationToken(name: ext.extendedType, navigationId: ext.definitionId, typeKind: .class, members: [])
+ let extNav = NavigationToken(name: ext.extendedType, navigationId: ext.definitionId!, typeKind: .class, members: [])
childItems.append(extNav)
}
// sort the navigation elements by name
diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift
index 27148b70de8..f57779b2689 100644
--- a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift
+++ b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift
@@ -101,13 +101,16 @@ class PackageModel: Tokenizable, Linkable {
}
}
- func tokenize(apiview a: APIViewModel, parent: Linkable?) {
- a.text("package")
- a.whitespace()
- a.text(name, definitionId: definitionId)
- a.punctuation("{", spacing: SwiftSyntax.TokenKind.leftBrace.spacing)
- a.newline()
+ func tokenize(apiview a: CodeModel, parent: Linkable?) {
+ var options = ReviewTokenOptions()
+ options.hasSuffixSpace = true
+ a.text("package", options: options)
+ a.lineMarker(definitionId)
+ a.text(name)
+ options.applySpacing(SwiftSyntax.TokenKind.leftBrace.spacing)
+ a.punctuation("{", options: options)
a.indent {
+ a.blankLines(set: 0)
for member in members {
member.tokenize(apiview: a, parent: self)
a.blankLines(set: 1)
@@ -115,19 +118,13 @@ class PackageModel: Tokenizable, Linkable {
// render any orphaned extensions
if !extensions.isEmpty {
a.comment("Non-package extensions")
- a.newline()
- let endIdx = extensions.count - 1
- for (idx, ext) in extensions.enumerated() {
- ext.tokenize(apiview: a, parent: nil)
- if idx != endIdx {
- a.blankLines(set: 1)
- }
- }
+ extensions.tokenize(apiview: a, parent: nil)
}
}
- a.punctuation("}", spacing: SwiftSyntax.TokenKind.rightBrace.spacing)
- a.newline()
- resolveTypeReferences(apiview: a)
+ a.blankLines(set: 0)
+ options.applySpacing(SwiftSyntax.TokenKind.rightBrace.spacing)
+ a.punctuation("}", options: options)
+ a.blankLines(set: 0)
}
/// Move extensions into the model representations for declared package types
@@ -144,13 +141,13 @@ class PackageModel: Tokenizable, Linkable {
self.extensions = otherExtensions
// process orphaned extensions
for ext in extensions {
- ext.processMembers(withParent: nil)
+ ext.processMembers()
}
extensions = extensions.resolveDuplicates()
// process all extensions associated with members
for member in members {
for ext in member.extensions {
- ext.processMembers(withParent: ext.parent)
+ ext.processMembers()
}
member.extensions = member.extensions.resolveDuplicates()
}
@@ -172,15 +169,6 @@ class PackageModel: Tokenizable, Linkable {
members = members.sorted(by: { $0.definitionId! < $1.definitionId! })
}
- /// attempt to resolve type references that are declared after they are used
- func resolveTypeReferences(apiview a: APIViewModel) {
- for (idx, token) in a.tokens.enumerated() {
- guard token.navigateToId == APIViewModel.unresolved else { continue }
- a.tokens[idx].navigateToId = a.definitionId(for: token.value!, withParent: nil)
- assert (a.tokens[idx].navigateToId != APIViewModel.unresolved)
- }
- }
-
func appendIfVisible(_ decl: DeclarationModel) {
if decl.shouldShow() {
members.append(decl)
@@ -196,13 +184,4 @@ class PackageModel: Tokenizable, Linkable {
}
return result.first
}
-
- func navigationTokenize(apiview a: APIViewModel, parent: Linkable?) {
- let packageToken = NavigationToken(name: name, navigationId: name, typeKind: .assembly, members: members)
- a.add(token: packageToken)
- if !extensions.isEmpty {
- let extensionsToken = NavigationToken(name: "Other Extensions", navigationId: "", typeKind: .assembly, extensions: extensions)
- a.add(token: extensionsToken)
- }
- }
}
diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift
new file mode 100644
index 00000000000..703e2a4d9a5
--- /dev/null
+++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift
@@ -0,0 +1,110 @@
+// --------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+// The MIT License (MIT)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the ""Software""), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//
+// --------------------------------------------------------------------------
+
+import Foundation
+import SwiftSyntax
+
+class ReviewLine: Tokenizable, Encodable {
+ /// Required to support commenting on a line. Usually code line for documentation or just punctuation is not required
+ /// to have lineId. It is used to link comments and navigation to specific lines. It must be a unique value or logic bugs
+ /// will manifest, but must also be deterministic (i.e. it cannot just be a random GUID).
+ var lineId: String?
+ /// whereas `lineId` is typically specific to the target language, `crossLanguageId` is the TypeSpec-based form of ID
+ /// so that concepts can be linked across different languages. Like `lineId`, it must be unique and deterministic.
+ var crossLanguageId: String?
+ /// The list of tokens that forms this particular review line.
+ var tokens: [ReviewToken] = []
+ /// Add any child lines as children. For e.g. all classes and namespace level methods are added as a children of a
+ /// namespace(module) level code line. Similarly all method level code lines are added as children of it's class
+ /// code line. Identation will automatically be applied to children.
+ var children: [ReviewLine] = []
+ /// Flag the line as hidden so it will not be shown to architects by default.
+ var isHidden: Bool?
+ /// Identifies that this line completes the existing context, usually the immediately previous reviewLine. For example,
+ /// in a class defintion that uses curly braces, the context begins with the class definition line and the closing curly brace
+ /// will be flagged as `isContextEndLine: True`.
+ var isContextEndLine: Bool?
+ /// Apply to any sibling-level `reviewLines` to mark that they are part of a specific context with the
+ /// matching `lineId`. This is used for lines that may print above or within a context that are not indented.
+ /// The final line of a context does not need this set. Instead, it should set `isContextEndLine`.
+ var relatedToLine: String?
+
+ /// Returns the text-based representation of all tokens
+ func text(indent: Int = 0) -> String {
+ let indentCount = 4
+ let indentString = String(repeating: " ", count: indent)
+ if tokens.count == 0 && children.count == 0 {
+ return "\n"
+ }
+ var value = indentString
+ for token in tokens {
+ value += token.text(withPreview: value)
+ }
+ if tokens.count > 0 {
+ value += "\n"
+ }
+ let childrenLines = self.children.map { $0.text(indent: indent + indentCount) }
+ for line in childrenLines {
+ value += line
+ }
+ return value
+ }
+
+ // MARK: Codable
+
+ enum CodingKeys: String, CodingKey {
+ case lineId = "LineId"
+ case crossLanguageId = "CrossLanguageId"
+ case tokens = "Tokens"
+ case children = "Children"
+ case isHidden = "IsHidden"
+ case isContextEndLine = "IsContextEndLine"
+ case relatedToLine = "RelatedToLine"
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(tokens, forKey: .tokens)
+ try container.encodeIfPresent(lineId, forKey: .lineId)
+ try container.encodeIfPresent(crossLanguageId, forKey: .crossLanguageId)
+ try container.encodeIfPresent(relatedToLine, forKey: .relatedToLine)
+ if (!children.isEmpty) {
+ try container.encode(children, forKey: .children)
+ }
+ if isHidden == true {
+ try container.encode(isHidden, forKey: .isHidden)
+ }
+ if isContextEndLine == true {
+ try container.encode(isContextEndLine, forKey: .isContextEndLine)
+ }
+ }
+
+ // MARK: Tokenizable
+
+ func tokenize(apiview: CodeModel, parent: (any Linkable)?) {
+ fatalError("Not implemented!")
+ }
+}
diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift
new file mode 100644
index 00000000000..ffbf1497153
--- /dev/null
+++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift
@@ -0,0 +1,147 @@
+// --------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+// The MIT License (MIT)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the ""Software""), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//
+// --------------------------------------------------------------------------
+
+import Foundation
+
+/// Enum for token kind
+enum TokenKind: Int, Codable {
+ /// plain text tokens for e.g documentation, namespace value, or attribute or decorator tokens. Most tokens will be text
+ case text = 0
+ /// punctuation
+ case punctuation = 1
+ /// language-specific keywords like `class`
+ case keyword = 2
+ /// class definitions, base class token, parameter types etc
+ case typeName = 3
+ /// method name tokens, member variable tokens
+ case memberName = 4
+ /// metadata or string literals to show in API view
+ case stringLiteral = 5
+ /// literals, for e.g. enum value or numerical constant literal or default value
+ case literal = 6
+ /// Comment text within the code that's really a documentation. Few languages wants to show comments within
+ /// API review that's not tagged as documentation.
+ case comment = 7
+}
+
+/// An individual token item
+class ReviewToken: Codable {
+ /// Text value
+ var value: String
+ /// Token kind
+ var kind: TokenKind
+ /// used to create a tree node in the navigation panel. Navigation nodes will be created only if this is set.
+ var navigationDisplayName: String?
+ /// navigate to the associated `lineId` when this token is clicked. (e.g. a param type which is class name in
+ /// the same package)
+ var navigateToId: String?
+ /// set to true if underlying token needs to be ignored from diff calculation. For e.g. package metadata or dependency versions
+ /// are usually excluded when comparing two revisions to avoid reporting them as API changes
+ var skipDiff: Bool? = false
+ /// set if API is marked as deprecated
+ var isDeprecated: Bool? = false
+ /// set to false if there is no suffix space required before next token. For e.g, punctuation right after method name
+ var hasSuffixSpace: Bool? = true
+ /// set to true if there is a prefix space required before current token. For e.g, space before token for =
+ var hasPrefixSpace: Bool? = false
+ /// set to true if current token is part of documentation
+ var isDocumentation: Bool? = false
+ /// Language-specific style css class names
+ var renderClasses: [String]?
+
+ init(kind: TokenKind, value: String, options: ReviewTokenOptions?) {
+ self.value = value
+ self.kind = kind
+ self.navigationDisplayName = options?.navigationDisplayName
+ self.navigateToId = options?.navigateToId
+ self.skipDiff = options?.skipDiff
+ self.isDeprecated = options?.isDeprecated
+ self.isDocumentation = options?.isDocumentation
+ self.hasSuffixSpace = options?.hasSuffixSpace
+ self.hasPrefixSpace = options?.hasPrefixSpace
+ self.renderClasses = options?.renderClasses
+ }
+
+ // MARK: Codable
+
+ enum CodingKeys: String, CodingKey {
+ case value = "Value"
+ case kind = "Kind"
+ case navigationDisplayName = "NavigationDisplayName"
+ case navigateToId = "NavigateToId"
+ case skipDiff = "SkipDiff"
+ case isDeprecated = "IsDeprecated"
+ case hasSuffixSpace = "HasSuffixSpace"
+ case hasPrefixSpace = "HasPrefixSpace"
+ case isDocumentation = "IsDocumentation"
+ case renderClasses = "RenderClasses"
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(value, forKey: .value)
+ try container.encode(kind.rawValue, forKey: .kind)
+ try container.encodeIfPresent(navigationDisplayName, forKey: .navigationDisplayName)
+ try container.encodeIfPresent(navigateToId, forKey: .navigateToId)
+ if skipDiff == true {
+ try container.encode(skipDiff, forKey: .skipDiff)
+ }
+ if isDeprecated == true {
+ try container.encode(isDeprecated, forKey: .isDeprecated)
+ }
+ if hasSuffixSpace == false {
+ try container.encode(hasSuffixSpace, forKey: .hasSuffixSpace)
+ }
+ if hasPrefixSpace == true {
+ try container.encode(hasPrefixSpace, forKey: .hasPrefixSpace)
+ }
+ if isDocumentation == true {
+ try container.encode(isDocumentation, forKey: .isDocumentation)
+ }
+ try container.encodeIfPresent(renderClasses, forKey: .renderClasses)
+ }
+
+ func text(withPreview preview: String) -> String {
+ let previewEndsInSpace = preview.hasSuffix(" ")
+ let hasSuffixSpace = self.hasSuffixSpace != nil ? self.hasSuffixSpace! : true
+ let hasPrefixSpace = self.hasPrefixSpace != nil ? self.hasPrefixSpace! : false
+ let suffixSpace = hasSuffixSpace ? " " : ""
+ let prefixSpace = (hasPrefixSpace && !previewEndsInSpace) ? " " : ""
+ return "\(prefixSpace)\(value)\(suffixSpace)"
+ }
+}
+
+extension Array {
+ var lastVisible: TokenKind? {
+ var values = self
+ while !values.isEmpty {
+ if let item = values.popLast() {
+ return item.kind
+ }
+ }
+ return nil
+ }
+}
diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift
new file mode 100644
index 00000000000..3e2601e5d7c
--- /dev/null
+++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift
@@ -0,0 +1,83 @@
+// --------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+// The MIT License (MIT)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the ""Software""), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//
+// --------------------------------------------------------------------------
+
+import Foundation
+
+/// Struct for setting reivew token options
+public struct ReviewTokenOptions {
+ /// NavigationDisplayName is used to create a tree node in the navigation panel. Navigation nodes will be created only if token
+ /// contains navigation display name.
+ var navigationDisplayName: String?
+
+ /// navigateToId should be set if the underlying token is required to be displayed as HREF to another type within the review.
+ /// For e.g. a param type which is class name in the same package
+ var navigateToId: String?
+
+ /// set skipDiff to true if underlying token needs to be ignored from diff calculation. For e.g. package metadata or dependency
+ /// versions are usually excluded when comparing two revisions to avoid reporting them as API changes
+ var skipDiff: Bool?
+
+ /// This is set if API is marked as deprecated
+ var isDeprecated: Bool?
+
+ /// Set this to true if a prefix space is required before the next value.
+ var hasPrefixSpace: Bool?
+
+ /// Set this to true if a suffix space required before next token. For e.g, punctuation right after method name
+ var hasSuffixSpace: Bool?
+
+ /// Set isDocumentation to true if current token is part of documentation
+ var isDocumentation: Bool?
+
+ /// Language specific style css class names
+ var renderClasses: [String]?
+
+ mutating func applySpacing(_ spacing: SpacingKind) {
+ switch spacing {
+ case .Leading:
+ hasPrefixSpace = true
+ hasSuffixSpace = false
+ case .Trailing:
+ hasPrefixSpace = false
+ hasSuffixSpace = true
+ case .Both:
+ hasPrefixSpace = true
+ hasSuffixSpace = true
+ case .Neither:
+ hasPrefixSpace = false
+ hasSuffixSpace = false
+ }
+ }
+}
+
+/// Struct for setting line marker options
+public struct LineMarkerOptions {
+ /// Flag to add the cross language ID
+ var addCrossLanguageId: Bool?
+
+ /// Related line ID
+ var relatedLineId: String?
+}
diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/Token.swift b/src/swift/SwiftAPIViewCore/Sources/Models/Token.swift
deleted file mode 100644
index 995a566d3a6..00000000000
--- a/src/swift/SwiftAPIViewCore/Sources/Models/Token.swift
+++ /dev/null
@@ -1,139 +0,0 @@
-// --------------------------------------------------------------------------
-//
-// Copyright (c) Microsoft Corporation. All rights reserved.
-//
-// The MIT License (MIT)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the ""Software""), to
-// deal in the Software without restriction, including without limitation the
-// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-// sell copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-// IN THE SOFTWARE.
-//
-// --------------------------------------------------------------------------
-
-import Foundation
-
-/// Enum for token kind
-enum TokenKind: Int, Codable {
- /// Text
- case text = 0
- /// Newline
- case newline = 1
- /// Whitespace
- case whitespace = 2
- /// Punctuation
- case punctuation = 3
- /// Swift keyword
- case keyword = 4
- /// used to display comment marker on lines with no visible tokens
- case lineIdMarker = 5
- /// Swift type name (class, structs, enums, etc.)
- case typeName = 6
- /// Variable names
- case memberName = 7
- /// Constants
- case stringLiteral = 8
- /// Literals
- case literal = 9
- /// Comments
- case comment = 10
- /// Document range start
- case documentRangeStart = 11
- /// Document range end
- case documentRangeEnd = 12
- /// Deprecated range start
- case deprecatedRangeStart = 13
- /// Deprecated range end
- case deprecatedRangeEnd = 14
- /// Start range to skip APIView diff
- case skipDiffRangeStart = 15
- /// End range to skip APIView diff
- case skippDiffRangeEnd = 16
-
- public var isVisible: Bool {
- switch self {
- case .text: return true
- case .newline: return true
- case .whitespace: return true
- case .punctuation: return true
- case .keyword: return true
- case .lineIdMarker: return false
- case .typeName: return true
- case .memberName: return true
- case .stringLiteral: return true
- case .literal: return true
- case .comment: return true
- case .documentRangeStart: return false
- case .documentRangeEnd: return false
- case .deprecatedRangeStart: return false
- case .deprecatedRangeEnd: return false
- case .skipDiffRangeStart: return false
- case .skippDiffRangeEnd: return false
- }
- }
-}
-
-/// An individual token item
-struct Token: Codable {
- /// Allows tokens to be navigated to. Should be unique. Used as ID for comment thread.
- var definitionId: String?
- /// If set, clicking on the token would navigate to the other token with this ID.
- var navigateToId: String?
- /// Text value
- var value: String?
- /// Token kind
- var kind: TokenKind
-
- // MARK: Codable
-
- enum CodingKeys: String, CodingKey {
- case definitionId = "DefinitionId"
- case navigateToId = "NavigateToId"
- case value = "Value"
- case kind = "Kind"
- }
-
- func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
- try container.encode(definitionId, forKey: .definitionId)
- try container.encode(navigateToId, forKey: .navigateToId)
- try container.encode(value, forKey: .value)
- try container.encode(kind, forKey: .kind)
- }
-
- var text: String {
- switch kind {
- case .lineIdMarker:
- return ""
- case .newline:
- return "\n"
- default:
- return value!
- }
- }
-}
-
-extension Array {
- var lastVisible: TokenKind? {
- var values = self
- while !values.isEmpty {
- if let item = values.popLast(), item.kind.isVisible {
- return item.kind
- }
- }
- return nil
- }
-}
diff --git a/src/swift/SwiftAPIViewCore/Sources/Protocols/Linkable.swift b/src/swift/SwiftAPIViewCore/Sources/Protocols/Linkable.swift
index aa2e2bd423e..b1dd83cc7f7 100644
--- a/src/swift/SwiftAPIViewCore/Sources/Protocols/Linkable.swift
+++ b/src/swift/SwiftAPIViewCore/Sources/Protocols/Linkable.swift
@@ -33,6 +33,4 @@ protocol Linkable {
var name: String { get }
var definitionId: String? { get }
var parent: Linkable? { get }
-
- func navigationTokenize(apiview: APIViewModel, parent: Linkable?)
}
diff --git a/src/swift/SwiftAPIViewCore/Sources/Protocols/Tokenizable.swift b/src/swift/SwiftAPIViewCore/Sources/Protocols/Tokenizable.swift
index edc41dff6c8..c38012dd210 100644
--- a/src/swift/SwiftAPIViewCore/Sources/Protocols/Tokenizable.swift
+++ b/src/swift/SwiftAPIViewCore/Sources/Protocols/Tokenizable.swift
@@ -28,5 +28,5 @@ import Foundation
/// Conforming objects can be serialized into APIView tokens.
protocol Tokenizable {
- func tokenize(apiview: APIViewModel, parent: Linkable?)
+ func tokenize(apiview: CodeModel, parent: Linkable?)
}
diff --git a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift
index ddc2846ba17..e309cfba246 100644
--- a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift
+++ b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift
@@ -28,7 +28,8 @@ import Foundation
import SwiftSyntax
extension SyntaxProtocol {
- func tokenize(apiview a: APIViewModel, parent: Linkable?) {
+ func tokenize(apiview a: CodeModel, parent: Linkable?) {
+ var options = ReviewTokenOptions()
let syntaxKind = self.kind
switch syntaxKind {
case .associatedtypeDecl:
@@ -42,14 +43,22 @@ extension SyntaxProtocol {
if child.childNameInParent == "name" {
let attrName = child.withoutTrivia().description
// don't add space if the attribute has parameters
- a.keyword(attrName, spacing: children.count == 2 ? .Trailing : .Neither)
+ if children.count == 2 {
+ options.applySpacing(.Trailing)
+ a.keyword(attrName, options: options)
+ } else {
+ options.applySpacing(.Neither)
+ a.keyword(attrName, options: options)
+ }
+
} else {
child.tokenize(apiview: a, parent: parent)
}
}
case .classRestrictionType:
// in this simple context, class should not have a trailing space
- a.keyword("class", spacing: .Neither)
+ options.applySpacing(.Neither)
+ a.keyword("class", options: options)
case .codeBlock:
// Don't render code blocks. APIView is unconcerned with implementation
break
@@ -60,7 +69,7 @@ extension SyntaxProtocol {
for child in children {
child.tokenize(apiview: a, parent: parent)
if (child.kind == .token) {
- a.whitespace()
+ a.currentLine.tokens.last?.hasSuffixSpace = true
}
}
case .enumCaseElement:
@@ -70,8 +79,9 @@ extension SyntaxProtocol {
if childIndex == 1 {
let token = TokenSyntax(child)!
if case let SwiftSyntax.TokenKind.identifier(label) = token.tokenKind {
- let defId = identifier(forName: label, withPrefix: parent?.definitionId)
- a.member(name: label, definitionId: defId)
+ let lineId = identifier(forName: label, withPrefix: parent?.definitionId)
+ a.lineMarker(lineId)
+ a.member(name: label)
} else {
SharedLogger.warn("Unhandled enum label kind '\(token.tokenKind)'. APIView may not display correctly.")
}
@@ -94,7 +104,8 @@ extension SyntaxProtocol {
// index 5 is the external name, which we always render as text
let token = TokenSyntax(child)!
if case let SwiftSyntax.TokenKind.identifier(val) = token.tokenKind {
- a.text(val)
+ options.hasSuffixSpace = false
+ a.text(val, options: options)
} else if case SwiftSyntax.TokenKind.wildcardKeyword = token.tokenKind {
a.text("_")
} else {
@@ -107,7 +118,7 @@ extension SyntaxProtocol {
let attrIndex = attrs.indexInParent
attr.tokenize(apiview: a, parent: parent)
if attrIndex != lastAttrs {
- a.whitespace()
+ a.currentLine.tokens.last?.hasSuffixSpace = true
}
}
} else {
@@ -117,21 +128,17 @@ extension SyntaxProtocol {
case .identifierPattern:
let name = IdentifierPatternSyntax(self)!.identifier.withoutTrivia().text
let lineId = identifier(forName: name, withPrefix: parent?.definitionId)
- a.member(name: name, definitionId: lineId)
+ a.lineMarker(lineId)
+ a.member(name: name)
case .initializerDecl:
DeclarationModel(from: InitializerDeclSyntax(self)!, parent: parent).tokenize(apiview: a, parent: parent)
case .memberDeclList:
a.indent {
- let beforeCount = a.tokens.count
- tokenizeChildren(apiview: a, parent: parent)
- // only render newline if tokens were actually added
- if a.tokens.count > beforeCount {
- a.newline()
- }
+ tokenizeMembers(apiview: a, parent: parent)
}
case .memberDeclListItem:
let decl = MemberDeclListItemSyntax(self)!.decl
- let publicModifiers = APIViewModel.publicModifiers
+ let publicModifiers = CodeModel.publicModifiers
var showDecl = publicModifiers.contains(decl.modifiers?.accessLevel ?? .unspecified)
switch decl.kind {
case .associatedtypeDecl:
@@ -151,7 +158,7 @@ extension SyntaxProtocol {
case .variableDecl:
// Public protocols should expose all members even if they have no access level modifier
if let parentDecl = (parent as? DeclarationModel), parentDecl.kind == .protocol {
- showDecl = showDecl || APIViewModel.publicModifiers.contains(parentDecl.accessLevel)
+ showDecl = showDecl || CodeModel.publicModifiers.contains(parentDecl.accessLevel)
}
default:
// show the unrecognized member by default
@@ -159,26 +166,20 @@ extension SyntaxProtocol {
showDecl = true
}
if showDecl {
- a.newline()
- tokenizeChildren(apiview: a, parent: parent)
- }
- case .precedenceGroupAttributeList:
- a.indent {
tokenizeChildren(apiview: a, parent: parent)
- a.newline()
}
case .precedenceGroupRelation:
- a.newline()
+ a.blankLines(set: 0)
if let name = PrecedenceGroupRelationSyntax(self)!.keyword {
let lineId = identifier(forName: name, withPrefix: parent?.definitionId)
- a.lineIdMarker(definitionId: lineId)
+ a.lineMarker(lineId)
}
tokenizeChildren(apiview: a, parent: parent)
case .precedenceGroupAssociativity:
- a.newline()
+ a.blankLines(set: 0)
if let name = PrecedenceGroupAssociativitySyntax(self)!.keyword {
let lineId = identifier(forName: name, withPrefix: parent?.definitionId)
- a.lineIdMarker(definitionId: lineId)
+ a.lineMarker(lineId)
}
tokenizeChildren(apiview: a, parent: parent)
case .subscriptDecl:
@@ -196,7 +197,8 @@ extension SyntaxProtocol {
let tokenKind = token.tokenKind
let tokenText = token.withoutTrivia().description
if tokenKind == .leftBrace || tokenKind == .rightBrace {
- a.punctuation(tokenText, spacing: .Both)
+ options.applySpacing(tokenKind.spacing)
+ a.punctuation(tokenText, options: options)
} else {
child.tokenize(token: token, apiview: a, parent: nil)
}
@@ -210,21 +212,35 @@ extension SyntaxProtocol {
}
}
- func tokenizeChildren(apiview a: APIViewModel, parent: Linkable?) {
+ func tokenizeMembers(apiview a: CodeModel, parent: Linkable?) {
+ let children = self.children(viewMode: .sourceAccurate)
+ let lastIdx = children.count - 1
+ for (idx, child) in children.enumerated() {
+ let beforeCount = a.currentLine.tokens.count
+ child.tokenize(apiview: a, parent: parent)
+ // skip if no tokens were actually added
+ guard (a.currentLine.tokens.count > beforeCount) else { continue }
+ a.blankLines(set: 0)
+ }
+ }
+
+ func tokenizeChildren(apiview a: CodeModel, parent: Linkable?) {
for child in self.children(viewMode: .sourceAccurate) {
child.tokenize(apiview: a, parent: parent)
}
}
- func tokenize(token: TokenSyntax, apiview a: APIViewModel, parent: DeclarationModel?) {
+ func tokenize(token: TokenSyntax, apiview a: CodeModel, parent: DeclarationModel?) {
let tokenKind = token.tokenKind
let tokenText = token.withoutTrivia().description
+ var options = ReviewTokenOptions()
+ options.applySpacing(tokenKind.spacing)
if tokenKind.isKeyword {
- a.keyword(tokenText, spacing: tokenKind.spacing)
+ a.keyword(tokenText, options: options)
return
} else if tokenKind.isPunctuation {
- a.punctuation(tokenText, spacing: tokenKind.spacing)
+ a.punctuation(tokenText, options: options)
return
}
if case let SwiftSyntax.TokenKind.identifier(val) = tokenKind {
@@ -232,23 +248,28 @@ extension SyntaxProtocol {
// used in @availabililty annotations
if nameInParent == "platform" {
a.text(tokenText)
- a.whitespace()
+ a.currentLine.tokens.last?.hasSuffixSpace = true
} else {
- a.typeReference(name: val, parent: parent)
+ a.typeReference(name: val, options: options)
}
} else if case let SwiftSyntax.TokenKind.spacedBinaryOperator(val) = tokenKind {
// in APIView, * is never used for multiplication
if val == "*" {
- a.punctuation(val, spacing: .Neither)
+ options.applySpacing(.Neither)
+ a.punctuation(val, options: options)
} else {
- a.punctuation(val, spacing: .Both)
+ options.applySpacing(.Both)
+ a.punctuation(val, options: options)
}
} else if case let SwiftSyntax.TokenKind.unspacedBinaryOperator(val) = tokenKind {
- a.punctuation(val, spacing: .Neither)
+ options.applySpacing(.Neither)
+ a.punctuation(val, options: options)
} else if case let SwiftSyntax.TokenKind.prefixOperator(val) = tokenKind {
- a.punctuation(val, spacing: .Leading)
+ options.applySpacing(.Leading)
+ a.punctuation(val, options: options)
} else if case let SwiftSyntax.TokenKind.postfixOperator(val) = tokenKind {
- a.punctuation(val, spacing: .Trailing)
+ options.applySpacing(.Trailing)
+ a.punctuation(val, options: options)
} else if case let SwiftSyntax.TokenKind.floatingLiteral(val) = tokenKind {
a.literal(val)
} else if case let SwiftSyntax.TokenKind.regexLiteral(val) = tokenKind {
@@ -258,7 +279,8 @@ extension SyntaxProtocol {
} else if case let SwiftSyntax.TokenKind.integerLiteral(val) = tokenKind {
a.literal(val)
} else if case let SwiftSyntax.TokenKind.contextualKeyword(val) = tokenKind {
- a.keyword(val, spacing: tokenKind.spacing)
+ options.applySpacing(tokenKind.spacing)
+ a.keyword(val, options: options)
} else if case let SwiftSyntax.TokenKind.stringSegment(val) = tokenKind {
a.text(val)
} else {
diff --git a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/TokenKind+Extensions.swift b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/TokenKind+Extensions.swift
index ae76f3fdf87..a7cd8bb7aa1 100644
--- a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/TokenKind+Extensions.swift
+++ b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/TokenKind+Extensions.swift
@@ -36,8 +36,6 @@ enum SpacingKind {
case Both
/// No spacing
case Neither
- /// chomps any leading whitespace to the left
- case TrimLeft
}
extension SwiftSyntax.TokenKind {
@@ -51,17 +49,15 @@ extension SwiftSyntax.TokenKind {
case .semicolon: return .Trailing
case .equal: return .Both
case .arrow: return .Both
- case .postfixQuestionMark: return .TrimLeft
case .leftBrace: return .Leading
case .initKeyword: return .Leading
case .wildcardKeyword: return .Neither
case let .contextualKeyword(val):
switch val {
- case "objc": return .TrimLeft
case "lowerThan", "higherThan", "associativity": return .Neither
case "available", "unavailable", "introduced", "deprecated", "obsoleted", "message", "renamed": return .Neither
case "willSet", "didSet", "get", "set":
- return .Leading
+ return .Both
default: return .Both
}
default:
diff --git a/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj b/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj
index f4e0a4cd368..c6cbcad07bd 100644
--- a/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj
+++ b/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj
@@ -7,15 +7,20 @@
objects = {
/* Begin PBXBuildFile section */
+ 0A61B5222D0B764600FC6B19 /* ReviewToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A61B5212D0B764600FC6B19 /* ReviewToken.swift */; };
+ 0A61B5232D0B764600FC6B19 /* CodeDiagnostic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A61B51F2D0B764600FC6B19 /* CodeDiagnostic.swift */; };
+ 0A61B5242D0B764600FC6B19 /* ReviewLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A61B5202D0B764600FC6B19 /* ReviewLine.swift */; };
+ 0A61B5272D0B76C400FC6B19 /* NamespaceStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A61B5252D0B76C400FC6B19 /* NamespaceStack.swift */; };
+ 0A61B5282D0B76C400FC6B19 /* ReviewTokenOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A61B5262D0B76C400FC6B19 /* ReviewTokenOptions.swift */; };
+ 0A61B52A2D0CADF300FC6B19 /* TestUtilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A61B5292D0CADE300FC6B19 /* TestUtilityTests.swift */; };
0A6C658A292D9EA00075C56F /* APIViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A846A1927879D0400C967A8 /* APIViewManager.swift */; };
0A6C658B292D9ED60075C56F /* CommandLineArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A846A0827879D0400C967A8 /* CommandLineArguments.swift */; };
0A6C658C292D9ED60075C56F /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A846A0C27879D0400C967A8 /* Errors.swift */; };
0A6C658D292D9ED60075C56F /* Identifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A84079827F3B34B00801E60 /* Identifiers.swift */; };
0A6C658E292D9ED60075C56F /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A846A0A27879D0400C967A8 /* Logger.swift */; };
- 0A6C658F292D9F8B0075C56F /* APIViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A84076527EE3DCE00801E60 /* APIViewModel.swift */; };
+ 0A6C658F292D9F8B0075C56F /* CodeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A84076527EE3DCE00801E60 /* CodeModel.swift */; };
0A6C6590292D9F8B0075C56F /* PackageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A84076327ED2CA400801E60 /* PackageModel.swift */; };
0A6C6591292D9FE60075C56F /* Tokenizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A84076127ED2A2500801E60 /* Tokenizable.swift */; };
- 0A6C6592292DA0090075C56F /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A846A1227879D0400C967A8 /* Token.swift */; };
0A6C6593292DA0140075C56F /* Linkable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A84076927EE4E2C00801E60 /* Linkable.swift */; };
0A6C6598292E98890075C56F /* SourceKittenFramework in Frameworks */ = {isa = PBXBuildFile; productRef = 0A6C6597292E98890075C56F /* SourceKittenFramework */; };
0A6C659F292EC8490075C56F /* NavigationTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A846A1127879D0400C967A8 /* NavigationTags.swift */; };
@@ -77,6 +82,12 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
+ 0A61B51F2D0B764600FC6B19 /* CodeDiagnostic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeDiagnostic.swift; sourceTree = ""; };
+ 0A61B5202D0B764600FC6B19 /* ReviewLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewLine.swift; sourceTree = ""; };
+ 0A61B5212D0B764600FC6B19 /* ReviewToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewToken.swift; sourceTree = ""; };
+ 0A61B5252D0B76C400FC6B19 /* NamespaceStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NamespaceStack.swift; sourceTree = ""; };
+ 0A61B5262D0B76C400FC6B19 /* ReviewTokenOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewTokenOptions.swift; sourceTree = ""; };
+ 0A61B5292D0CADE300FC6B19 /* TestUtilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtilityTests.swift; sourceTree = ""; };
0A76BF8A294A9A11007C776E /* TokenKind+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TokenKind+Extensions.swift"; sourceTree = ""; };
0A76BF8E294B8BCD007C776E /* SyntaxProtocol+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SyntaxProtocol+Extensions.swift"; sourceTree = ""; };
0A76BF90294B940A007C776E /* DeclarationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclarationModel.swift; sourceTree = ""; };
@@ -85,7 +96,7 @@
0A76BF96294BA4E3007C776E /* ModifierListSyntax+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ModifierListSyntax+Extensions.swift"; sourceTree = ""; };
0A84076127ED2A2500801E60 /* Tokenizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tokenizable.swift; sourceTree = ""; };
0A84076327ED2CA400801E60 /* PackageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageModel.swift; sourceTree = ""; };
- 0A84076527EE3DCE00801E60 /* APIViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIViewModel.swift; sourceTree = ""; };
+ 0A84076527EE3DCE00801E60 /* CodeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeModel.swift; sourceTree = ""; };
0A84076927EE4E2C00801E60 /* Linkable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Linkable.swift; sourceTree = ""; };
0A84079827F3B34B00801E60 /* Identifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identifiers.swift; sourceTree = ""; };
0A8469E127879AE200C967A8 /* SwiftAPIViewCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftAPIViewCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -95,7 +106,6 @@
0A846A0A27879D0400C967A8 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; };
0A846A0C27879D0400C967A8 /* Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; };
0A846A1127879D0400C967A8 /* NavigationTags.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationTags.swift; sourceTree = ""; };
- 0A846A1227879D0400C967A8 /* Token.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; };
0A846A1327879D0400C967A8 /* NavigationToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationToken.swift; sourceTree = ""; };
0A846A1927879D0400C967A8 /* APIViewManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIViewManager.swift; sourceTree = ""; };
0A846A342787A88E00C967A8 /* CoreInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = CoreInfo.plist; sourceTree = ""; };
@@ -199,9 +209,10 @@
0A846A0227879D0400C967A8 /* Tests */ = {
isa = PBXGroup;
children = (
- B2DA7DFF2D0A0A6A0059E51F /* ExpectFiles */,
B2DA7E212D0A0A6F0059E51F /* TestFiles */,
+ B2DA7DFF2D0A0A6A0059E51F /* ExpectFiles */,
0A846A0327879D0400C967A8 /* SwiftAPIViewCoreTests.swift */,
+ 0A61B5292D0CADE300FC6B19 /* TestUtilityTests.swift */,
);
path = Tests;
sourceTree = "";
@@ -209,11 +220,11 @@
0A846A0627879D0400C967A8 /* Sources */ = {
isa = PBXGroup;
children = (
- 0A76BF89294A99FB007C776E /* SwiftSyntax+Extensions */,
- 0A846A1927879D0400C967A8 /* APIViewManager.swift */,
- 0A84077A27EE659100801E60 /* Protocols */,
0A846A0F27879D0400C967A8 /* Models */,
+ 0A84077A27EE659100801E60 /* Protocols */,
+ 0A76BF89294A99FB007C776E /* SwiftSyntax+Extensions */,
0A846A0927879D0400C967A8 /* Util */,
+ 0A846A1927879D0400C967A8 /* APIViewManager.swift */,
);
path = Sources;
sourceTree = "";
@@ -233,13 +244,17 @@
isa = PBXGroup;
children = (
0A76BF94294BA10A007C776E /* AccessLevel.swift */,
- 0A84076527EE3DCE00801E60 /* APIViewModel.swift */,
+ 0A61B51F2D0B764600FC6B19 /* CodeDiagnostic.swift */,
+ 0A84076527EE3DCE00801E60 /* CodeModel.swift */,
0A76BF90294B940A007C776E /* DeclarationModel.swift */,
0AA1BFBD2953839E00AE8C11 /* ExtensionModel.swift */,
+ 0A61B5252D0B76C400FC6B19 /* NamespaceStack.swift */,
0A84076327ED2CA400801E60 /* PackageModel.swift */,
+ 0A61B5202D0B764600FC6B19 /* ReviewLine.swift */,
+ 0A61B5212D0B764600FC6B19 /* ReviewToken.swift */,
+ 0A61B5262D0B76C400FC6B19 /* ReviewTokenOptions.swift */,
0A846A1127879D0400C967A8 /* NavigationTags.swift */,
0A846A1327879D0400C967A8 /* NavigationToken.swift */,
- 0A846A1227879D0400C967A8 /* Token.swift */,
);
path = Models;
sourceTree = "";
@@ -423,14 +438,18 @@
files = (
0A76BF8B294A9A11007C776E /* TokenKind+Extensions.swift in Sources */,
0A6C659F292EC8490075C56F /* NavigationTags.swift in Sources */,
+ 0A61B5222D0B764600FC6B19 /* ReviewToken.swift in Sources */,
+ 0A61B5232D0B764600FC6B19 /* CodeDiagnostic.swift in Sources */,
+ 0A61B5272D0B76C400FC6B19 /* NamespaceStack.swift in Sources */,
+ 0A61B5282D0B76C400FC6B19 /* ReviewTokenOptions.swift in Sources */,
+ 0A61B5242D0B764600FC6B19 /* ReviewLine.swift in Sources */,
0A6C65A0292EC8490075C56F /* NavigationToken.swift in Sources */,
0A76BF97294BA4E3007C776E /* ModifierListSyntax+Extensions.swift in Sources */,
0A6C6593292DA0140075C56F /* Linkable.swift in Sources */,
0AA1BFBE2953839E00AE8C11 /* ExtensionModel.swift in Sources */,
- 0A6C6592292DA0090075C56F /* Token.swift in Sources */,
0A6C6591292D9FE60075C56F /* Tokenizable.swift in Sources */,
0A76BF93294B987C007C776E /* DeclSyntaxProtocol+Extensions.swift in Sources */,
- 0A6C658F292D9F8B0075C56F /* APIViewModel.swift in Sources */,
+ 0A6C658F292D9F8B0075C56F /* CodeModel.swift in Sources */,
0A76BF91294B940A007C776E /* DeclarationModel.swift in Sources */,
0A6C6590292D9F8B0075C56F /* PackageModel.swift in Sources */,
0A6C658B292D9ED60075C56F /* CommandLineArguments.swift in Sources */,
@@ -458,6 +477,7 @@
buildActionMask = 2147483647;
files = (
0A846A2F27879E7800C967A8 /* SwiftAPIViewCoreTests.swift in Sources */,
+ 0A61B52A2D0CADF300FC6B19 /* TestUtilityTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -616,7 +636,7 @@
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
- MARKETING_VERSION = 0.2.2;
+ MARKETING_VERSION = 0.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.SwiftAPIViewCore;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
@@ -646,7 +666,7 @@
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
- MARKETING_VERSION = 0.2.2;
+ MARKETING_VERSION = 0.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.SwiftAPIViewCore;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
diff --git a/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/xcshareddata/xcschemes/SwiftAPIViewCore.xcscheme b/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/xcshareddata/xcschemes/SwiftAPIViewCore.xcscheme
index e261e3ac84c..029a0b9eb57 100644
--- a/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/xcshareddata/xcschemes/SwiftAPIViewCore.xcscheme
+++ b/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/xcshareddata/xcschemes/SwiftAPIViewCore.xcscheme
@@ -27,8 +27,13 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
- shouldUseLaunchSchemeArgsEnv = "YES"
- shouldAutocreateTestPlan = "YES">
+ shouldUseLaunchSchemeArgsEnv = "YES">
+
+
+
+
diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/AttributesExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/AttributesExpectFile.txt
index bf627ece4f1..477f6fc548f 100644
--- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/AttributesExpectFile.txt
+++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/AttributesExpectFile.txt
@@ -1,4 +1,4 @@
-Package parsed using Swift APIView (version 0.2.2)
+Package parsed using Swift APIView (version 0.3.0)
package AttributesTestFile.swifttxt {
diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/EnumerationsExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/EnumerationsExpectFile.txt
index ebb5393a1a5..d9c94efc438 100644
--- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/EnumerationsExpectFile.txt
+++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/EnumerationsExpectFile.txt
@@ -1,4 +1,4 @@
-Package parsed using Swift APIView (version 0.2.2)
+Package parsed using Swift APIView (version 0.3.0)
package EnumerationsTestFile.swifttxt {
diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ExtensionExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ExtensionExpectFile.txt
index 761c119f743..63c65be8402 100644
--- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ExtensionExpectFile.txt
+++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ExtensionExpectFile.txt
@@ -1,4 +1,4 @@
-Package parsed using Swift APIView (version 0.2.2)
+Package parsed using Swift APIView (version 0.3.0)
package ExtensionTestFile.swifttxt {
@@ -20,6 +20,7 @@ package ExtensionTestFile.swifttxt {
}
\\ Non-package extensions
+
public extension Double {
var km: Double
var m: Double
diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/FunctionsExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/FunctionsExpectFile.txt
index f112f44fab0..f4836026c3c 100644
--- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/FunctionsExpectFile.txt
+++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/FunctionsExpectFile.txt
@@ -1,4 +1,4 @@
-Package parsed using Swift APIView (version 0.2.2)
+Package parsed using Swift APIView (version 0.3.0)
package FunctionsTestFile.swifttxt {
diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/GenericsExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/GenericsExpectFile.txt
index d409390ee80..0e3e9bfc041 100644
--- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/GenericsExpectFile.txt
+++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/GenericsExpectFile.txt
@@ -1,4 +1,4 @@
-Package parsed using Swift APIView (version 0.2.2)
+Package parsed using Swift APIView (version 0.3.0)
package GenericsTestFile.swifttxt {
diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/InitializersExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/InitializersExpectFile.txt
index 4cf4f163072..c6fc17c307c 100644
--- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/InitializersExpectFile.txt
+++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/InitializersExpectFile.txt
@@ -1,4 +1,4 @@
-Package parsed using Swift APIView (version 0.2.2)
+Package parsed using Swift APIView (version 0.3.0)
package InitializersTestFile.swifttxt {
diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/OperatorExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/OperatorExpectFile.txt
index af4116e3a2b..9785a607484 100644
--- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/OperatorExpectFile.txt
+++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/OperatorExpectFile.txt
@@ -1,4 +1,4 @@
-Package parsed using Swift APIView (version 0.2.2)
+Package parsed using Swift APIView (version 0.3.0)
package OperatorTestFile.swifttxt {
diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/PrivateInternalExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/PrivateInternalExpectFile.txt
index f3f4d9ffb29..f37345d2353 100644
--- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/PrivateInternalExpectFile.txt
+++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/PrivateInternalExpectFile.txt
@@ -1,5 +1,4 @@
-Package parsed using Swift APIView (version 0.2.2)
+Package parsed using Swift APIView (version 0.3.0)
-package PrivateInternalTestFile.swifttxt {
-}
+package PrivateInternalTestFile.swifttxt {}
diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/PropertiesExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/PropertiesExpectFile.txt
index 82dbc9a6234..cf2ab521e30 100644
--- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/PropertiesExpectFile.txt
+++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/PropertiesExpectFile.txt
@@ -1,4 +1,4 @@
-Package parsed using Swift APIView (version 0.2.2)
+Package parsed using Swift APIView (version 0.3.0)
package PropertiesTestFile.swifttxt {
diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ProtocolExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ProtocolExpectFile.txt
index 644808185e5..8d444a328e8 100644
--- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ProtocolExpectFile.txt
+++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ProtocolExpectFile.txt
@@ -1,4 +1,4 @@
-Package parsed using Swift APIView (version 0.2.2)
+Package parsed using Swift APIView (version 0.3.0)
package ProtocolTestFile.swifttxt {
@@ -38,8 +38,7 @@ package ProtocolTestFile.swifttxt {
public var textualDescription: String
}
- extension Hamster: TextRepresentable {
- }
+ extension Hamster: TextRepresentable {}
public protocol Named {
var name: String { get }
@@ -110,6 +109,7 @@ package ProtocolTestFile.swifttxt {
public func wishHappyBirthday(to: Named & Aged)
\\ Non-package extensions
+
extension Array: TextRepresentable where Element: TextRepresentable {
public var textualDescription: String
}
diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/SwiftUIExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/SwiftUIExpectFile.txt
index ec87bba046b..26b2bc5725c 100644
--- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/SwiftUIExpectFile.txt
+++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/SwiftUIExpectFile.txt
@@ -1,9 +1,10 @@
-Package parsed using Swift APIView (version 0.2.2)
+Package parsed using Swift APIView (version 0.3.0)
package SwiftUITestFile.swifttxt {
public class ViewBuilderExample {
public func testViewBuilder(@ViewBuilder content: () -> Content)
+
@ViewBuilder
public func createView() -> some View
}
diff --git a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift
index e34345db9d7..26e1d9c5777 100644
--- a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift
+++ b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift
@@ -29,6 +29,18 @@ import XCTest
class SwiftAPIViewCoreTests: XCTestCase {
+ /// Simple structure to track validation metadata on `ReviewLine`
+ struct ReviewLineData: Equatable {
+ /// Counts the number of `relatedLineId`
+ var relatedToCount: Int;
+ /// Counts the number of `isContextEndLine`
+ var isContextEndCount: Int;
+
+ var description: String {
+ return "relatedToCount: \(relatedToCount), isContextEndCount: \(isContextEndCount)"
+ }
+ }
+
override func setUpWithError() throws {
try super.setUpWithError()
continueAfterFailure = false
@@ -50,6 +62,7 @@ class SwiftAPIViewCoreTests: XCTestCase {
return try! String(contentsOfFile: path)
}
+ /// Compares the text syntax of the APIView against what is expected.
private func compare(expected: String, actual: String) {
let actualLines = actual.split(separator: "\n", omittingEmptySubsequences: false).map { String($0) }
let expectedLines = expected.split(separator: "\n", omittingEmptySubsequences: false).map { String($0) }
@@ -63,6 +76,111 @@ class SwiftAPIViewCoreTests: XCTestCase {
XCTAssertEqual(actualLines.count, expectedLines.count, "Number of lines does not match")
}
+ /// Ensure there are no duplicate line IDs in the review, as that would lead
+ /// to functional bugs on the web.
+ private func validateLineIds(apiview: CodeModel) {
+
+ func validate(line: ReviewLine) {
+ // ensure first token does not have prefix space
+ // and last does not have suffix space
+ if let firstToken = line.tokens.first {
+ if firstToken.hasPrefixSpace ?? false {
+ XCTFail("Unexpected prefix space on first token")
+ }
+ }
+
+ if let lastToken = line.tokens.last {
+ if lastToken.hasSuffixSpace ?? false {
+ XCTFail("Unexpected suffix space on last token")
+ }
+ }
+
+ // ensure there are no repeated definition IDs
+ if let lineId = line.lineId {
+ if lineIds.contains(lineId) {
+ XCTFail("Duplicate line ID: \(lineId)")
+ }
+ if lineId != "" {
+ lineIds.insert(lineId)
+ }
+ for child in line.children {
+ validate(line: child)
+ }
+ }
+ }
+
+ var lineIds = Set()
+ for line in apiview.reviewLines {
+ validate(line: line)
+ }
+ }
+
+ /// Extracts related lines from the APIView to ensure proper collapsing behavior on the web.
+ private func getRelatedLineMetadata(apiview: CodeModel) -> [String: ReviewLineData] {
+ /// Extracts the `ReviewLineData` for the provided review lines.
+ func getReviewLinesMetadata(lines: [ReviewLine]?) -> [String: ReviewLineData]? {
+ guard let lines = lines else { return nil }
+ guard !lines.isEmpty else { return nil }
+ var mainMap = [String: ReviewLineData]()
+ var lastKey: String? = nil
+ for line in lines {
+ let lineId = line.lineId
+ if let related = line.relatedToLine {
+ lastKey = related
+ var subMap = mainMap[related] ?? ReviewLineData(relatedToCount: 0, isContextEndCount: 0)
+ subMap.relatedToCount += 1
+ mainMap[related] = subMap
+ }
+ if line.isContextEndLine == true {
+ guard lastKey != nil else {
+ XCTFail("isEndContext found without a related line.")
+ return nil
+ }
+ var subMap = mainMap[lastKey!] ?? ReviewLineData(relatedToCount: 0, isContextEndCount: 0)
+ subMap.isContextEndCount += 1
+ mainMap[lastKey!] = subMap
+ }
+ if !line.children.isEmpty {
+ guard lineId != nil else {
+ XCTFail("Child without a line ID.")
+ return nil
+ }
+ lastKey = lineId
+ if let subMap = getReviewLinesMetadata(lines: line.children) {
+ for (key, value) in subMap {
+ mainMap[key] = value
+ }
+ }
+ }
+ lastKey = lineId
+ }
+ return mainMap
+ }
+ let countMap = getReviewLinesMetadata(lines: apiview.reviewLines)
+ return countMap ?? [String: ReviewLineData]()
+ }
+
+ /// Compare `ReviewLineData` information for equality
+ func compareCounts(_ lhs: [String: ReviewLineData], _ rhs: [String: ReviewLineData]) {
+ // ensure keys are the same
+ let lhsKeys = Set(lhs.keys)
+ let rhsKeys = Set(rhs.keys)
+ let combined = lhsKeys.union(rhsKeys)
+ if (combined.count != lhsKeys.count) {
+ XCTFail("Key mismatch: \(lhsKeys.description) vs \(rhsKeys.description)")
+ return
+ }
+ for key in lhs.keys {
+ let lhsVal = lhs[key]!
+ let rhsVal = rhs[key]!
+ if lhsVal != rhsVal {
+ XCTFail("Value mismatch for key \(key): \(lhsVal.description) vs \(rhsVal.description)")
+ }
+ }
+ }
+
+ // MARK: Tests
+
func testAttributes() throws {
let manager = APIViewManager(mode: .testing)
manager.config.sourcePath = pathFor(testFile: "AttributesTestFile")
@@ -70,6 +188,17 @@ class SwiftAPIViewCoreTests: XCTestCase {
let generated = try manager.run()
let expected = contentsOf(expectFile: "AttributesExpectFile")
compare(expected: expected, actual: generated)
+ validateLineIds(apiview: manager.model!)
+ let counts = getRelatedLineMetadata(apiview: manager.model!)
+ let expectedCounts: [String: ReviewLineData] = [
+ "AttributesTestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "AttributesTestFile.swifttxt.ExampleClass": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "AttributesTestFile.swifttxt.MyClass": ReviewLineData(relatedToCount: 1, isContextEndCount: 0),
+ "AttributesTestFile.swifttxt.MyProtocol": ReviewLineData(relatedToCount: 1, isContextEndCount: 0),
+ "AttributesTestFile.swifttxt.MyStruct": ReviewLineData(relatedToCount: 2, isContextEndCount: 0),
+ "AttributesTestFile.swifttxt.SomeSendable": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ ]
+ compareCounts(counts, expectedCounts)
}
func testEnumerations() throws {
@@ -79,6 +208,18 @@ class SwiftAPIViewCoreTests: XCTestCase {
let generated = try manager.run()
let expected = contentsOf(expectFile: "EnumerationsExpectFile")
compare(expected: expected, actual: generated)
+ validateLineIds(apiview: manager.model!)
+ let counts = getRelatedLineMetadata(apiview: manager.model!)
+ let expectedCounts: [String: ReviewLineData] = [
+ "EnumerationsTestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "EnumerationsTestFile.swifttxt.ASCIIControlCharacter": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "EnumerationsTestFile.swifttxt.ArithmeticExpression": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "EnumerationsTestFile.swifttxt.Barcode": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "EnumerationsTestFile.swifttxt.CompassPoint": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "EnumerationsTestFile.swifttxt.Planet": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ ]
+ compareCounts(counts, expectedCounts)
+
}
func testExtensions() throws {
@@ -88,6 +229,21 @@ class SwiftAPIViewCoreTests: XCTestCase {
let generated = try manager.run()
let expected = contentsOf(expectFile: "ExtensionExpectFile")
compare(expected: expected, actual: generated)
+ validateLineIds(apiview: manager.model!)
+ let counts = getRelatedLineMetadata(apiview: manager.model!)
+ let expectedCounts: [String: ReviewLineData] = [
+ "ExtensionTestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ExtensionTestFile.swifttxt.Point": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ExtensionTestFile.swifttxt.Rect": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionRect": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ExtensionTestFile.swifttxt.Size": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionDouble": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionInt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionInt.Kind": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionStack": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionStackwhereElement:Equatable": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ ]
+ compareCounts(counts, expectedCounts)
}
func testFunctions() throws {
@@ -97,6 +253,13 @@ class SwiftAPIViewCoreTests: XCTestCase {
let generated = try manager.run()
let expected = contentsOf(expectFile: "FunctionsExpectFile")
compare(expected: expected, actual: generated)
+ validateLineIds(apiview: manager.model!)
+ let counts = getRelatedLineMetadata(apiview: manager.model!)
+ let expectedCounts: [String: ReviewLineData] = [
+ "FunctionsTestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "FunctionsTestFile.swifttxt.FunctionTestClass": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ ]
+ compareCounts(counts, expectedCounts)
}
func testGenerics() throws {
@@ -106,6 +269,26 @@ class SwiftAPIViewCoreTests: XCTestCase {
let generated = try manager.run()
let expected = contentsOf(expectFile: "GenericsExpectFile")
compare(expected: expected, actual: generated)
+ validateLineIds(apiview: manager.model!)
+ let counts = getRelatedLineMetadata(apiview: manager.model!)
+ let expectedCounts: [String: ReviewLineData] = [
+ "GenericsTestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "GenericsTestFile.swifttxt.Container": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "GenericsTestFile.swifttxt.ContainerAlt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "GenericsTestFile.swifttxt.ContainerStack": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "GenericsTestFile.swifttxt.IntContainerStack": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "GenericsTestFile.swifttxt.Shape": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "GenericsTestFile.swifttxt.Square": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "GenericsTestFile.swifttxt.Stack": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "GenericsTestFile.swifttxt.SuffixableContainer": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionContainer": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionContainerwhereItem==Double": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionContainerwhereItem:Equatable": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionContainerStack:SuffixableContainer": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionIntContainerStack:SuffixableContainer": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionStack": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ ]
+ compareCounts(counts, expectedCounts)
}
func testInitializers() throws {
@@ -115,6 +298,13 @@ class SwiftAPIViewCoreTests: XCTestCase {
let generated = try manager.run()
let expected = contentsOf(expectFile: "InitializersExpectFile")
compare(expected: expected, actual: generated)
+ validateLineIds(apiview: manager.model!)
+ let counts = getRelatedLineMetadata(apiview: manager.model!)
+ let expectedCounts: [String: ReviewLineData] = [
+ "InitializersTestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "InitializersTestFile.swifttxt.InitializersTestClass": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ ]
+ compareCounts(counts, expectedCounts)
}
func testOperators() throws {
@@ -124,6 +314,16 @@ class SwiftAPIViewCoreTests: XCTestCase {
let generated = try manager.run()
let expected = contentsOf(expectFile: "OperatorExpectFile")
compare(expected: expected, actual: generated)
+ validateLineIds(apiview: manager.model!)
+ let counts = getRelatedLineMetadata(apiview: manager.model!)
+ let expectedCounts: [String: ReviewLineData] = [
+ "OperatorTestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "OperatorTestFile.swifttxt.CongruentPrecedence": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "OperatorTestFile.swifttxt.Vector2D": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionVector2D": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionVector2D:Equatable": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ ]
+ compareCounts(counts, expectedCounts)
}
func testPrivateInternal() throws {
@@ -133,6 +333,10 @@ class SwiftAPIViewCoreTests: XCTestCase {
let generated = try manager.run()
let expected = contentsOf(expectFile: "PrivateInternalExpectFile")
compare(expected: expected, actual: generated)
+ validateLineIds(apiview: manager.model!)
+ let counts = getRelatedLineMetadata(apiview: manager.model!)
+ let expectedCounts: [String: ReviewLineData] = [:]
+ compareCounts(counts, expectedCounts)
}
func testProperties() throws {
@@ -142,6 +346,13 @@ class SwiftAPIViewCoreTests: XCTestCase {
let generated = try manager.run()
let expected = contentsOf(expectFile: "PropertiesExpectFile")
compare(expected: expected, actual: generated)
+ validateLineIds(apiview: manager.model!)
+ let counts = getRelatedLineMetadata(apiview: manager.model!)
+ let expectedCounts: [String: ReviewLineData] = [
+ "PropertiesTestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "PropertiesTestFile.swifttxt.PropertiesTestClass": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ ]
+ compareCounts(counts, expectedCounts)
}
func testProtocols() throws {
@@ -151,6 +362,38 @@ class SwiftAPIViewCoreTests: XCTestCase {
let generated = try manager.run()
let expected = contentsOf(expectFile: "ProtocolExpectFile")
compare(expected: expected, actual: generated)
+ validateLineIds(apiview: manager.model!)
+ let counts = getRelatedLineMetadata(apiview: manager.model!)
+ let expectedCounts: [String: ReviewLineData] = [
+ "ProtocolTestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.Aged": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.ComposedPerson": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.CounterDataSource": ReviewLineData(relatedToCount: 1, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.Dice": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionDice:TextRepresentable": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.FullyNamed": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.Hamster": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.Named": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.OnOffSwitch": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.Person": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.PrettyTextRepresentable": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionPrettyTextRepresentable": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.RandomNumberGenerator": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionRandomNumberGenerator": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.SomeClass": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.SomeInitProtocol": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.SomeOtherInitProtocol": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.SomeProtocol": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.SomeSubClass": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.SomeSuperClass": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.TextRepresentable": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.Togglable": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionArray:TextRepresentablewhereElement:TextRepresentable": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "extensionCollectionwhereElement:Equatable": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "ProtocolTestFile.swifttxt.CounterDataSource.increment(forCount:Int)->Int": ReviewLineData(relatedToCount: 1, isContextEndCount: 0)
+
+ ]
+ compareCounts(counts, expectedCounts)
}
func testSwiftUI() throws {
@@ -160,5 +403,13 @@ class SwiftAPIViewCoreTests: XCTestCase {
let generated = try manager.run()
let expected = contentsOf(expectFile: "SwiftUIExpectFile")
compare(expected: expected, actual: generated)
+ validateLineIds(apiview: manager.model!)
+ let counts = getRelatedLineMetadata(apiview: manager.model!)
+ let expectedCounts: [String: ReviewLineData] = [
+ "SwiftUITestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "SwiftUITestFile.swifttxt.ViewBuilderExample": ReviewLineData(relatedToCount: 0, isContextEndCount: 1),
+ "SwiftUITestFile.swifttxt.ViewBuilderExample.createView()->someView": ReviewLineData(relatedToCount: 1, isContextEndCount: 0),
+ ]
+ compareCounts(counts, expectedCounts)
}
}
diff --git a/src/swift/SwiftAPIViewCore/Tests/TestUtilityTests.swift b/src/swift/SwiftAPIViewCore/Tests/TestUtilityTests.swift
new file mode 100644
index 00000000000..adab56db1c7
--- /dev/null
+++ b/src/swift/SwiftAPIViewCore/Tests/TestUtilityTests.swift
@@ -0,0 +1,134 @@
+// --------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+// The MIT License (MIT)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the ""Software""), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//
+// --------------------------------------------------------------------------
+
+import XCTest
+@testable import SwiftAPIViewCore
+
+class TestUtilityTests: XCTestCase {
+
+ override func setUpWithError() throws {
+ try super.setUpWithError()
+ continueAfterFailure = false
+ SharedLogger.set(logger: NullLogger(), withLevel: .info)
+ }
+
+ private func compare(expected: String, actual: String) {
+ let actualLines = actual.split(separator: "\n", omittingEmptySubsequences: false).map { String($0) }
+ let expectedLines = expected.split(separator: "\n", omittingEmptySubsequences: false).map { String($0) }
+ for (i, expected) in expectedLines.enumerated() {
+ let actual = actualLines[i]
+ if (actual == expected) {
+ continue
+ }
+ XCTFail("Line \(i): (\(actual) is not equal to (\(expected)")
+ }
+ XCTAssertEqual(actualLines.count, expectedLines.count, "Number of lines does not match")
+ }
+
+ func testReviewLineText() throws {
+ let line = ReviewLine()
+ var options = ReviewTokenOptions()
+ options.hasSuffixSpace = false
+ options.hasPrefixSpace = false
+ line.tokens = [ReviewToken(kind: .text, value: "Some text", options: options)]
+ let model = CodeModel(packageName: "Test", packageVersion: "0.0", statements: [])
+ model.reviewLines = [line, ReviewLine(), ReviewLine(), line]
+ let generated = model.text
+ let expected = "Some text\n\n\nSome text\n"
+ compare(expected: expected, actual: generated)
+ }
+
+ func testReviewLineTextWithChildren() throws {
+ let model = CodeModel(packageName: "Test", packageVersion: "0.0", statements: [])
+ var options = ReviewTokenOptions()
+ options.hasSuffixSpace = false
+ options.hasPrefixSpace = false
+ let line1 = ReviewLine()
+ line1.tokens = [ReviewToken(kind: .text, value: "Some text", options: options)]
+
+ let line2 = ReviewLine()
+ line2.tokens = [ReviewToken(kind: .text, value: "public class Foo()", options: options)]
+ let child1 = ReviewLine()
+ child1.tokens = [ReviewToken(kind: .text, value: "func getFoo() -> Foo", options: options)]
+ let child2 = ReviewLine()
+ let child3 = ReviewLine()
+ child3.tokens = [ReviewToken(kind: .text, value: "func setFoo(_: Foo)", options: options)]
+ line2.children = [child1, child2, child3]
+ model.reviewLines = [line1, ReviewLine(), ReviewLine(), line2]
+ let generated = model.text
+ let expected = "Some text\n\n\npublic class Foo()\n func getFoo() -> Foo\n\n func setFoo(_: Foo)\n"
+ compare(expected: expected, actual: generated)
+ }
+
+ func testSuffixSpaceBehavior() throws {
+ let model = CodeModel(packageName: "Test", packageVersion: "0.0", statements: [])
+ let line = ReviewLine()
+ var options = ReviewTokenOptions()
+ options.hasPrefixSpace = false
+ line.tokens = [ReviewToken(kind: .text, value: "A", options: options)]
+ options.hasSuffixSpace = true
+ line.tokens.append(ReviewToken(kind: .text, value: "B", options: options))
+ options.hasSuffixSpace = false
+ line.tokens.append(ReviewToken(kind: .text, value: "C", options: options))
+ model.reviewLines = [line]
+ let generated = model.text
+ let expected = "A B C\n"
+ compare(expected: expected, actual: generated)
+ }
+
+ func testPrefixSpaceBehavior() throws {
+ let model = CodeModel(packageName: "Test", packageVersion: "0.0", statements: [])
+ let line1 = ReviewLine()
+ var options = ReviewTokenOptions()
+ options.hasSuffixSpace = false
+ line1.tokens = [ReviewToken(kind: .text, value: "A", options: options)]
+ options.hasPrefixSpace = true
+ line1.tokens.append(ReviewToken(kind: .text, value: "B", options: options))
+ options.hasPrefixSpace = false
+ line1.tokens.append(ReviewToken(kind: .text, value: "C", options: options))
+
+ let line2 = ReviewLine()
+ options = ReviewTokenOptions()
+ options.hasPrefixSpace = true
+ options.hasSuffixSpace = nil
+ line2.tokens = [ReviewToken(kind: .text, value: "A", options: options)]
+ line2.tokens.append(ReviewToken(kind: .text, value: "B", options: options))
+ line2.tokens.append(ReviewToken(kind: .text, value: "C", options: options))
+
+ let line3 = ReviewLine()
+ options = ReviewTokenOptions()
+ options.hasPrefixSpace = true
+ options.hasSuffixSpace = true
+ line3.tokens = [ReviewToken(kind: .text, value: "A", options: options)]
+ line3.tokens.append(ReviewToken(kind: .text, value: "B", options: options))
+ line3.tokens.append(ReviewToken(kind: .text, value: "C", options: options))
+
+ model.reviewLines = [line1, line2, line3]
+ let generated = model.text
+ let expected = "A BC\n A B C \n A B C \n"
+ compare(expected: expected, actual: generated)
+ }
+}
diff --git a/src/swift/SwiftAPIViewTests/Sources/SwiftAPIViewTests.h b/src/swift/SwiftAPIViewTests/Sources/SwiftAPIViewTests.h
deleted file mode 100644
index 6d4116f12b3..00000000000
--- a/src/swift/SwiftAPIViewTests/Sources/SwiftAPIViewTests.h
+++ /dev/null
@@ -1,18 +0,0 @@
-//
-// SwiftAPIViewTests.h
-// SwiftAPIViewTests
-//
-// Created by Travis Prescott on 3/30/22.
-//
-
-#import
-
-//! Project version number for SwiftAPIViewTests.
-FOUNDATION_EXPORT double SwiftAPIViewTestsVersionNumber;
-
-//! Project version string for SwiftAPIViewTests.
-FOUNDATION_EXPORT const unsigned char SwiftAPIViewTestsVersionString[];
-
-// In this header, you should import all the public headers of your framework using statements like #import
-
-
diff --git a/src/swift/SwiftAPIViewTests/SwiftAPIViewTests.xcodeproj/project.pbxproj b/src/swift/SwiftAPIViewTests/SwiftAPIViewTests.xcodeproj/project.pbxproj
index 90e0fc6e8a2..a9325ef3879 100644
--- a/src/swift/SwiftAPIViewTests/SwiftAPIViewTests.xcodeproj/project.pbxproj
+++ b/src/swift/SwiftAPIViewTests/SwiftAPIViewTests.xcodeproj/project.pbxproj
@@ -13,7 +13,6 @@
0A34D5C327FF40A3008A76A6 /* EnumerationsTestFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A34D5BC27FF40A2008A76A6 /* EnumerationsTestFile.swift */; };
0A34D5C427FF40A3008A76A6 /* ProtocolTestFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A34D5BD27FF40A2008A76A6 /* ProtocolTestFile.swift */; };
0A34D5C527FF40A3008A76A6 /* GenericsTestFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A34D5BE27FF40A2008A76A6 /* GenericsTestFile.swift */; };
- 0A34D5C627FF40A3008A76A6 /* SwiftAPIViewTests.h in Headers */ = {isa = PBXBuildFile; fileRef = 0A34D5BF27FF40A2008A76A6 /* SwiftAPIViewTests.h */; };
0A34D5C727FF40A3008A76A6 /* OperatorTestFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A34D5C027FF40A2008A76A6 /* OperatorTestFile.swift */; };
0A34D5C927FF40AF008A76A6 /* FunctionsTestFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A34D5C827FF40AF008A76A6 /* FunctionsTestFile.swift */; };
0A35C20028298ED0008C99AD /* PropertiesTestFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A35C1FF28298ED0008C99AD /* PropertiesTestFile.swift */; };
@@ -39,7 +38,6 @@
0A34D5BC27FF40A2008A76A6 /* EnumerationsTestFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnumerationsTestFile.swift; sourceTree = ""; };
0A34D5BD27FF40A2008A76A6 /* ProtocolTestFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProtocolTestFile.swift; sourceTree = ""; };
0A34D5BE27FF40A2008A76A6 /* GenericsTestFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericsTestFile.swift; sourceTree = ""; };
- 0A34D5BF27FF40A2008A76A6 /* SwiftAPIViewTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SwiftAPIViewTests.h; sourceTree = ""; };
0A34D5C027FF40A2008A76A6 /* OperatorTestFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperatorTestFile.swift; sourceTree = ""; };
0A34D5C827FF40AF008A76A6 /* FunctionsTestFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FunctionsTestFile.swift; sourceTree = ""; };
0A35C1FF28298ED0008C99AD /* PropertiesTestFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertiesTestFile.swift; sourceTree = ""; };
@@ -81,7 +79,6 @@
0A76BF9A294BB193007C776E /* PrivateInternalTestFile.swift */,
0A35C1FF28298ED0008C99AD /* PropertiesTestFile.swift */,
0A34D5BD27FF40A2008A76A6 /* ProtocolTestFile.swift */,
- 0A34D5BF27FF40A2008A76A6 /* SwiftAPIViewTests.h */,
0A2E6B4F28EF4F1B001F313D /* SwiftUITestFile.swift */,
);
path = Sources;
@@ -111,7 +108,6 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
- 0A34D5C627FF40A3008A76A6 /* SwiftAPIViewTests.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};