From 342f6cf217e1e7d15ca7d1944f3db006be1581ee Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Fri, 6 Dec 2024 11:48:57 -0800 Subject: [PATCH 01/29] Initial work. --- .../Sources/Models/APIViewModel.swift | 107 ++++++++------ .../Sources/Models/CodeDiagnostic.swift | 82 +++++++++++ .../Sources/Models/ReviewLine.swift | 83 +++++++++++ .../Sources/Models/ReviewToken.swift | 124 ++++++++++++++++ .../Sources/Models/Token.swift | 139 ------------------ .../project.pbxproj | 24 ++- 6 files changed, 365 insertions(+), 194 deletions(-) create mode 100644 src/swift/SwiftAPIViewCore/Sources/Models/CodeDiagnostic.swift create mode 100644 src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift create mode 100644 src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift delete mode 100644 src/swift/SwiftAPIViewCore/Sources/Models/Token.swift diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift index d07d37fb276..695a20cfdf8 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -30,26 +30,37 @@ 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 + /// The package version + var packageVersion: String + + /// Version of the APIView language parser used to create the token file + let parserVersion: String - /// Language string. + /// Language discriminator let language = "Swift" - /// The APIView tokens to display - var tokens: [Token] + /// 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] - /// The navigation tokens to display - var navigation: [NavigationToken] + /// System generated comments. Each is linked to a review line ID. + var diagnostics: [CodeDiagnostic]? + + /// Navigation items are used to create a tree view in the Navigation panel. + /// Each item is linked to a review line ID. If omitted, the Navigation panel + /// will automatically be generated using the review lines. + var navigation: [NavigationToken]? = nil /// Node-based representation of the Swift package - var model: PackageModel + private var model: PackageModel /// Current indentation level private var indentLevel = 0 @@ -76,12 +87,15 @@ class APIViewModel: Tokenizable, Encodable { // MARK: Initializers - init(name: String, packageName: String, versionString: String, statements: [CodeBlockItemSyntax.Item]) { - self.name = name - self.versionString = versionString + 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 - navigation = [NavigationToken]() - tokens = [Token]() + self.parserVersion = bundle.object(forInfoDictionaryKey: versionKey) as? String ?? "Unknown" + reviewLines = [ReviewLine]() + diagnostics = [CodeDiagnostic]() model = PackageModel(name: packageName, statements: statements) self.tokenize(apiview: self, parent: nil) model.navigationTokenize(apiview: self, parent: nil) @@ -90,37 +104,39 @@ class APIViewModel: Tokenizable, Encodable { // MARK: Codable enum CodingKeys: String, CodingKey { - case name = "Name" - case tokens = "Tokens" - case language = "Language" 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" - 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(packageVersion, forKey: .packageVersion) + try container.encode(parserVersion, forKey: .parserVersion) try container.encode(language, forKey: .language) - try container.encode(tokens, forKey: .tokens) - try container.encode(navigation, forKey: .navigation) - try container.encode(versionString, forKey: .versionString) + try container.encode(languageVariant, forKey: .languageVariant) + try container.encode(reviewLines, forKey: .reviewLines) + try container.encodeIfPresent(crossLanguagePackageId, forKey: .crossLanguagePackageId) + try container.encodeIfPresent(diagnostics, forKey: .diagnostics) + try container.encodeIfPresent(navigation, forKey: .navigation) } 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.text("Package parsed using Swift APIView (version \(self.parserVersion))") a.newline() a.blankLines(set: 2) model.tokenize(apiview: a, parent: parent) } // MARK: Token Emitters - func add(token: Token) { + func add(token: ReviewToken) { self.tokens.append(token) } @@ -130,7 +146,7 @@ class APIViewModel: Tokenizable, Encodable { func text(_ text: String, definitionId: String? = nil) { checkIndent() - let item = Token(definitionId: definitionId, navigateToId: nil, value: text, kind: .text) + let item = ReviewToken(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) @@ -150,7 +166,7 @@ class APIViewModel: Tokenizable, Encodable { 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) + let item = ReviewToken(definitionId: nil, navigateToId: nil, value: nil, kind: .newline) add(token: item) } needsIndent = true @@ -172,7 +188,7 @@ class APIViewModel: Tokenizable, Encodable { // if not enough newlines, add some let linesToAdd = (count + 1) - newlineCount for _ in 0.. (count + 1) { // if there are too many newlines, remove some @@ -188,7 +204,7 @@ class APIViewModel: Tokenizable, Encodable { // don't double up on whitespace guard tokens.lastVisible != .whitespace else { return } let value = String(repeating: " ", count: count) - let item = Token(definitionId: nil, navigateToId: nil, value: value, kind: .whitespace) + let item = ReviewToken(definitionId: nil, navigateToId: nil, value: value, kind: .whitespace) add(token: item) } @@ -200,7 +216,7 @@ class APIViewModel: Tokenizable, Encodable { if [SpacingKind.Leading, SpacingKind.Both].contains(spacing) { self.whitespace() } - let item = Token(definitionId: nil, navigateToId: nil, value: value, kind: .punctuation) + let item = ReviewToken(definitionId: nil, navigateToId: nil, value: value, kind: .punctuation) add(token: item) if [SpacingKind.Trailing, SpacingKind.Both].contains(spacing) { self.whitespace() @@ -215,7 +231,7 @@ class APIViewModel: Tokenizable, Encodable { if [SpacingKind.Leading, SpacingKind.Both].contains(spacing) { self.whitespace() } - let item = Token(definitionId: nil, navigateToId: nil, value: keyword, kind: .keyword) + let item = ReviewToken(definitionId: nil, navigateToId: nil, value: keyword, kind: .keyword) add(token: item) if [SpacingKind.Trailing, SpacingKind.Both].contains(spacing) { self.whitespace() @@ -225,7 +241,7 @@ class APIViewModel: Tokenizable, Encodable { /// Create a line ID marker (only needed if no other token has a definition ID) func lineIdMarker(definitionId: String?) { checkIndent() - let item = Token(definitionId: definitionId, navigateToId: nil, value: nil, kind: .lineIdMarker) + let item = ReviewToken(definitionId: definitionId, navigateToId: nil, value: nil, kind: .lineIdMarker) add(token: item) } @@ -236,7 +252,7 @@ class APIViewModel: Tokenizable, Encodable { return } checkIndent() - let item = Token(definitionId: definitionId, navigateToId: definitionId, value: name, kind: .typeName) + let item = ReviewToken(definitionId: definitionId, navigateToId: definitionId, value: name, kind: .typeName) definitionIds.insert(definitionId) add(token: item) } @@ -255,11 +271,8 @@ class APIViewModel: Tokenizable, Encodable { /// 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) + let item = ReviewToken(definitionId: nil, navigateToId: linkId, value: name, kind: .typeName) add(token: item) } @@ -285,7 +298,7 @@ class APIViewModel: Tokenizable, Encodable { func member(name: String, definitionId: String? = nil) { checkIndent() - let item = Token(definitionId: definitionId, navigateToId: nil, value: name, kind: .memberName) + let item = ReviewToken(definitionId: definitionId, navigateToId: nil, value: name, kind: .memberName) add(token: item) } @@ -299,17 +312,17 @@ class APIViewModel: Tokenizable, Encodable { if !text.starts(with: "\\") { message = "\\\\ \(message)" } - let item = Token(definitionId: nil, navigateToId: nil, value: message, kind: .comment) + let item = ReviewToken(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) + let item = ReviewToken(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) + let item = ReviewToken(definitionId: nil, navigateToId: nil, value: "\"\(text)\"", kind: .stringLiteral) add(token: item) } @@ -345,7 +358,7 @@ class APIViewModel: Tokenizable, Encodable { /// Trims whitespace tokens func trim(removeNewlines: Bool = false) { - var lineIds = [Token]() + var lineIds = [ReviewToken]() while (!tokens.isEmpty) { var continueTrim = true if let kind = tokens.last?.kind { diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/CodeDiagnostic.swift b/src/swift/SwiftAPIViewCore/Sources/Models/CodeDiagnostic.swift new file mode 100644 index 00000000000..a86aac05844 --- /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: APIViewModel, parent: (any Linkable)?) { + fatalError("Not implemented!") + } +} diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift new file mode 100644 index 00000000000..e1e4c3be48a --- /dev/null +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.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 +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? + + // 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(children, forKey: .children) + try container.encodeIfPresent(isHidden, forKey: .isHidden) + try container.encodeIfPresent(isContextEndLine, forKey: .isContextEndLine) + try container.encodeIfPresent(relatedToLine, forKey: .relatedToLine) + } + + // MARK: Tokenizable + + func tokenize(apiview: APIViewModel, 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..ad66f121640 --- /dev/null +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift @@ -0,0 +1,124 @@ +// -------------------------------------------------------------------------- +// +// 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(value: String, kind: TokenKind) { + self.value = value + self.kind = kind + } + + // 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) + try container.encodeIfPresent(skipDiff, forKey: .skipDiff) + try container.encodeIfPresent(isDeprecated, forKey: .isDeprecated) + try container.encodeIfPresent(hasSuffixSpace, forKey: .hasSuffixSpace) + try container.encodeIfPresent(hasPrefixSpace, forKey: .hasPrefixSpace) + try container.encodeIfPresent(isDocumentation, forKey: .isDocumentation) + try container.encodeIfPresent(renderClasses, forKey: .renderClasses) + } + + var text: String { + return value + } +} + +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/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/SwiftAPIViewCore.xcodeproj/project.pbxproj b/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj index f4e0a4cd368..7ff4f5f769d 100644 --- a/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj +++ b/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ 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 */; }; 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 */; }; @@ -15,7 +18,6 @@ 0A6C658F292D9F8B0075C56F /* APIViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A84076527EE3DCE00801E60 /* APIViewModel.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 +79,9 @@ /* 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 = ""; }; 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 = ""; }; @@ -95,7 +100,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,8 +203,8 @@ 0A846A0227879D0400C967A8 /* Tests */ = { isa = PBXGroup; children = ( - B2DA7DFF2D0A0A6A0059E51F /* ExpectFiles */, B2DA7E212D0A0A6F0059E51F /* TestFiles */, + B2DA7DFF2D0A0A6A0059E51F /* ExpectFiles */, 0A846A0327879D0400C967A8 /* SwiftAPIViewCoreTests.swift */, ); path = Tests; @@ -209,11 +213,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 = ""; @@ -234,12 +238,14 @@ children = ( 0A76BF94294BA10A007C776E /* AccessLevel.swift */, 0A84076527EE3DCE00801E60 /* APIViewModel.swift */, + 0A61B51F2D0B764600FC6B19 /* CodeDiagnostic.swift */, 0A76BF90294B940A007C776E /* DeclarationModel.swift */, 0AA1BFBD2953839E00AE8C11 /* ExtensionModel.swift */, 0A84076327ED2CA400801E60 /* PackageModel.swift */, + 0A61B5202D0B764600FC6B19 /* ReviewLine.swift */, + 0A61B5212D0B764600FC6B19 /* ReviewToken.swift */, 0A846A1127879D0400C967A8 /* NavigationTags.swift */, 0A846A1327879D0400C967A8 /* NavigationToken.swift */, - 0A846A1227879D0400C967A8 /* Token.swift */, ); path = Models; sourceTree = ""; @@ -423,11 +429,13 @@ files = ( 0A76BF8B294A9A11007C776E /* TokenKind+Extensions.swift in Sources */, 0A6C659F292EC8490075C56F /* NavigationTags.swift in Sources */, + 0A61B5222D0B764600FC6B19 /* ReviewToken.swift in Sources */, + 0A61B5232D0B764600FC6B19 /* CodeDiagnostic.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 */, From 025ef8929fa64dcc63c81436a3a3e5a87f24e245 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Mon, 9 Dec 2024 16:45:21 -0800 Subject: [PATCH 02/29] Convert internal methods. --- .../Sources/Models/APIViewModel.swift | 322 +++++++++--------- .../Sources/Models/DeclarationModel.swift | 2 +- .../Sources/Models/ReviewToken.swift | 10 +- .../Sources/Models/ReviewTokenOptions.swift | 57 ++++ .../SyntaxProtocol+Extensions.swift | 4 +- 5 files changed, 236 insertions(+), 159 deletions(-) create mode 100644 src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift index 695a20cfdf8..80beb3f4524 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -62,15 +62,6 @@ class APIViewModel: Tokenizable, Encodable { /// Node-based representation of the Swift package private 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] @@ -80,6 +71,15 @@ class APIViewModel: Tokenizable, Encodable { /// Tracks assigned definition IDs so they can be linked private var definitionIds = Set() + /// Stores the current line. All helper methods append to this. + private var currentLine: ReviewLine + + /// Stores the parent of the current line. + private var currentParent: ReviewLine? + + /// Stores the stack of parent lines + private var parentStack: [ReviewLine] + /// Returns the text-based representation of all tokens var text: String { return tokens.map { $0.text }.joined() @@ -136,127 +136,104 @@ class APIViewModel: Tokenizable, Encodable { } // MARK: Token Emitters - func add(token: ReviewToken) { - self.tokens.append(token) + func token(kind: TokenKind, value: String, options: ReviewTokenOptions?) { + let token = ReviewToken(kind: kind, value: value, options: options) + self.currentLine.tokens.append(token) } + // FIXME: We are going to eliminate this essentially... func add(token: NavigationToken) { self.navigation.append(token) } - func text(_ text: String, definitionId: String? = nil) { - checkIndent() - let item = ReviewToken(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) + func text(_ text: String, options: ReviewTokenOptions?) { + self.token(kind: .text, value: text, options: options) } - /// 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) - } + // ensure no trailing space at the end of the line + if self.currentLine.tokens.count > 0 { + let lastToken = currentLine.tokens.last + lastToken?.hasSuffixSpace = false + let firstToken = currentLine.tokens.first + firstToken?.hasPrefixSpace = false } - checkIndent() - // don't add newline if one already in place - if tokens.last?.kind != .newline { - let item = ReviewToken(definitionId: nil, navigateToId: nil, value: nil, kind: .newline) - add(token: item) + + if let currentParent = self.currentParent { + currentParent.children.append(self.currentLine) + } else { + self.reviewLines.append(self.currentLine) } - needsIndent = true + self.currentLine = ReviewLine() } - /// Ensures a specific number of blank lines. Will add or remove newline - /// tokens as needed to ensure the exact number of blank lines. + /// Set the exact number of desired newlines. func blankLines(set count: Int) { + self.newline() + var parentLines = self.currentParent?.children ?? self.reviewLines // count the number of trailing newlines var newlineCount = 0 - for token in self.tokens.reversed() { - if token.kind == .newline { + for line in self.reviewLines.reversed() { + if line.tokens.count == 0 { 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 newlineCount == count { + return + } else if (newlineCount > count) { // if there are too many newlines, remove some - let linesToRemove = newlineCount - (count + 1) + let linesToRemove = newlineCount - count for _ in 0.. DeclarationModel? { guard let item = item else { return nil } switch item.kind { @@ -269,13 +246,13 @@ class APIViewModel: Tokenizable, Encodable { } /// Link to a registered type - func typeReference(name: String, parent: DeclarationModel?) { - checkIndent() - let linkId = definitionId(for: name, withParent: parent) ?? APIViewModel.unresolved - let item = ReviewToken(definitionId: nil, navigateToId: linkId, value: name, kind: .typeName) - add(token: item) + func typeReference(name: String, options: ReviewTokenOptions?) {\ + var newOptions = options ?? ReviewTokenOptions() + newOptions.navigateToId = options?.navigateToId ?? "__MISSING__" + self.token(kind: .typeName, value: name, options: newOptions) } + // FIXME: This! func definitionId(for val: String, withParent parent: DeclarationModel?) -> String? { var matchVal = val if !matchVal.contains("."), let parentObject = findNonFunctionParent(from: parent), let parentDefId = parentObject.definitionId { @@ -296,55 +273,75 @@ class APIViewModel: Tokenizable, Encodable { return matches.first } - func member(name: String, definitionId: String? = nil) { - checkIndent() - let item = ReviewToken(definitionId: definitionId, navigateToId: nil, value: name, kind: .memberName) - add(token: item) + func member(name: String, options: ReviewTokenOptions?) { + self.token(kind: .memberName, value: name, options: options) } - // TODO: Add support for diagnostics -// func diagnostic(self, text, line_id): -// self.diagnostics.append(Diagnostic(line_id, text)) + func diagnostic(message: String, targetId: String, level: CodeDiagnosticLevel, helpLink: String?) { + let diagnostic = CodeDiagnostic(message, targetId: targetId, level: level, helpLink: helpLink) + self.diagnostics = self.diagnostics ?? [] + self.diagnostics?.append(diagnostic) + } - func comment(_ text: String) { - checkIndent() + func comment(_ text: String, options: ReviewTokenOptions?) { var message = text if !text.starts(with: "\\") { message = "\\\\ \(message)" } - let item = ReviewToken(definitionId: nil, navigateToId: nil, value: message, kind: .comment) - add(token: item) + self.token(kind: .comment, value: message, options: options) } - func literal(_ value: String) { - let item = ReviewToken(definitionId: nil, navigateToId: nil, value: value, kind: .literal) - add(token: item) + func literal(_ value: String, options: ReviewTokenOptions?) { + self.token(kind: .literal, value: value, options: options) } - func stringLiteral(_ text: String) { - let item = ReviewToken(definitionId: nil, navigateToId: nil, value: "\"\(text)\"", kind: .stringLiteral) - add(token: item) + func stringLiteral(_ text: String, options: ReviewTokenOptions?) { + let lines = text.split("\n") + if lines.count > 1 { + self.currentLine.tokens.append(ReviewToken(kind: .stringLiteral, value: "\u0022\(text)\u0022"), options: options) + } else { + self.punctuation("\"\"\"", options: options) + self.newline() + for line in lines { + self.literal(line, options: options) + self.newline() + } + self.punctuation("\"\"\"", options: options) + } } - /// 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() + func indent(_ indentedLines: () -> Void) { + // ensure no trailing space at the end of the line + let lastToken = self.currentLine.tokens.last + lastToken?.hasSuffixSpace = false + + if let currentParent = self.currentParent { + currentParent.children?.append(self.currentLine) + self.parentStack.append(currentParent) + } else { + self.reviewLines.append(self.currentLine) } - indentLevel -= 1 - } + self.currentParent = self.currentLine + self.currentLine = ReviewLine() + + // handle the indented bodies + indentedLines() - /// Checks if indentation is needed and adds whitespace as needed - func checkIndent() { - guard needsIndent else { return } - whitespace(count: indentLevel * indentSpaces) - needsIndent = false + // 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() } + // FIXME: This! /// 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 @@ -356,38 +353,53 @@ class APIViewModel: Tokenizable, Encodable { return defId } - /// Trims whitespace tokens - func trim(removeNewlines: Bool = false) { - var lineIds = [ReviewToken]() - 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 + /// 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.currentLine.tokens.append(target) + return } - } - if !continueTrim { - break + 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) + } else { + // no snapping, so render in place + self.currentLine.tokens.append(target) + return + } + default: + // no snapping, so render in place + self.currentLine.tokens.append(target) + return } } - // reappend the line id tokens - while (!lineIds.isEmpty) { - if let popped = lineIds.popLast() { - tokens.append(popped) + } + + /// Retrieves the last line from the review + func getLastLine() -> ReviewLine? { + guard let currentParent = self.currentParent else { return nil } + let lastChild = currentParent.children?.last + let lastGrandchild = lastChild?.children?.last + if let greatGrandChildren = lastGrandchild?.children { + if greatGrandChildren.count > 0 { + fatalError("Unexpected great-grandchild in getLastLine()!") } } + return lastGrandchild ?? lastChild } } diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift index d83817d0098..43a4d775588 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift @@ -156,7 +156,7 @@ class DeclarationModel: Tokenizable, Linkable, Equatable { 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: "\(definitionId!).\(attrText)") attr.tokenize(apiview: a, parent: parent) a.newline() a.blankLines(set: 0) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift index ad66f121640..52ff6137b68 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift @@ -72,9 +72,17 @@ class ReviewToken: Codable { /// Language-specific style css class names var renderClasses: [String]? - init(value: String, kind: TokenKind) { + 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 diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift new file mode 100644 index 00000000000..c19123bba0d --- /dev/null +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift @@ -0,0 +1,57 @@ +// -------------------------------------------------------------------------- +// +// 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 +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]? +} diff --git a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift index ddc2846ba17..7d010af2c40 100644 --- a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift +++ b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift @@ -171,14 +171,14 @@ extension SyntaxProtocol { a.newline() if let name = PrecedenceGroupRelationSyntax(self)!.keyword { let lineId = identifier(forName: name, withPrefix: parent?.definitionId) - a.lineIdMarker(definitionId: lineId) + a.lineMarker(definitionId: lineId) } tokenizeChildren(apiview: a, parent: parent) case .precedenceGroupAssociativity: a.newline() if let name = PrecedenceGroupAssociativitySyntax(self)!.keyword { let lineId = identifier(forName: name, withPrefix: parent?.definitionId) - a.lineIdMarker(definitionId: lineId) + a.lineMarker(definitionId: lineId) } tokenizeChildren(apiview: a, parent: parent) case .subscriptDecl: From 397f58e26024ba97cbd106bf53e0ec4f27f26530 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Tue, 10 Dec 2024 10:39:01 -0800 Subject: [PATCH 03/29] Make APIViewModel compile. --- .../Sources/Models/APIViewModel.swift | 153 +++++++++--------- .../Sources/Models/NamespaceStack.swift | 53 ++++++ .../Sources/Models/ReviewLine.swift | 5 + .../Sources/Models/ReviewTokenOptions.swift | 64 +++++++- 4 files changed, 199 insertions(+), 76 deletions(-) create mode 100644 src/swift/SwiftAPIViewCore/Sources/Models/NamespaceStack.swift diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift index 80beb3f4524..c99f95fdd29 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -54,11 +54,6 @@ class APIViewModel: Tokenizable, Encodable { /// System generated comments. Each is linked to a review line ID. var diagnostics: [CodeDiagnostic]? - /// Navigation items are used to create a tree view in the Navigation panel. - /// Each item is linked to a review line ID. If omitted, the Navigation panel - /// will automatically be generated using the review lines. - var navigation: [NavigationToken]? = nil - /// Node-based representation of the Swift package private var model: PackageModel @@ -68,6 +63,9 @@ class APIViewModel: Tokenizable, Encodable { /// 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() @@ -75,14 +73,20 @@ class APIViewModel: Tokenizable, Encodable { private var currentLine: ReviewLine /// Stores the parent of the current line. - private var currentParent: ReviewLine? + private var currentParent: ReviewLine? = nil /// Stores the stack of parent lines - private var parentStack: [ReviewLine] + private var parentStack: [ReviewLine] = [] + + /// Used to track the currently processed namespace. + private var namespaceStack = NamespaceStack() - /// Returns the text-based representation of all tokens + /// Keeps track of which types have been declared. + private var typeDeclarations = Set() + + /// Returns the text-based representation of all lines var text: String { - return tokens.map { $0.text }.joined() + return reviewLines.map { $0.text }.joined() } // MARK: Initializers @@ -94,9 +98,12 @@ class APIViewModel: Tokenizable, Encodable { 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) model.navigationTokenize(apiview: self, parent: nil) } @@ -125,7 +132,6 @@ class APIViewModel: Tokenizable, Encodable { try container.encode(reviewLines, forKey: .reviewLines) try container.encodeIfPresent(crossLanguagePackageId, forKey: .crossLanguagePackageId) try container.encodeIfPresent(diagnostics, forKey: .diagnostics) - try container.encodeIfPresent(navigation, forKey: .navigation) } func tokenize(apiview a: APIViewModel, parent: Linkable?) { @@ -136,17 +142,12 @@ class APIViewModel: Tokenizable, Encodable { } // MARK: Token Emitters - func token(kind: TokenKind, value: String, options: ReviewTokenOptions?) { + func token(kind: TokenKind, value: String, options: ReviewTokenOptions? = nil) { let token = ReviewToken(kind: kind, value: value, options: options) self.currentLine.tokens.append(token) } - // FIXME: We are going to eliminate this essentially... - func add(token: NavigationToken) { - self.navigation.append(token) - } - - func text(_ text: String, options: ReviewTokenOptions?) { + func text(_ text: String, options: ReviewTokenOptions? = nil) { self.token(kind: .text, value: text, options: options) } @@ -160,7 +161,8 @@ class APIViewModel: Tokenizable, Encodable { } if let currentParent = self.currentParent { - currentParent.children.append(self.currentLine) + currentParent.children = currentParent.children ?? [] + currentParent.children!.append(self.currentLine) } else { self.reviewLines.append(self.currentLine) } @@ -197,83 +199,83 @@ class APIViewModel: Tokenizable, Encodable { } } - func punctuation(_ value: String, options: ReviewTokenOptions?) { - let snapTo = options?.snapTo - let isContextEndLine = options?.isContextEndLine - + func punctuation(_ value: String, options: PunctuationOptions? = nil) { let token = ReviewToken(kind: .punctuation, value: value, options: options) - - if snapTo { + if let snapTo = options?.snapTo { self.snap(token: token, to: snapTo) } else { self.currentLine.tokens.append(token) } - if isContextEndLine { + if let isContextEndLine = options?.isContextEndLine { self.currentLine.isContextEndLine = true } } - func keyword(_ keyword: String, options: ReviewTokenOptions?) { + func keyword(_ keyword: String, options: ReviewTokenOptions? = nil) { self.token(kind: .keyword, value: keyword, options: options) } - // FIXME: THIS! - func lineMarker(value: String?, addCrossLanguageId: Bool?, relatedLineId: String?) { + func lineMarker(_ options: LineMarkerOptions) { + self.currentLine.lineId = options.value ?? self.namespaceStack.value() + if options.addCrossLanguageId == true { + self.currentLine.crossLanguageId = options.value ?? self.namespaceStack.value() + } + self.currentLine.relatedToLine = options.relatedLineId } /// Register the declaration of a new type - func typeDeclaration(name: String, typeId: String?, addCrossLanguageId: Bool = false, options: ReviewTokenOptions?) { + func typeDeclaration(name: String, typeId: String?, addCrossLanguageId: Bool = false, options: ReviewTokenOptions? = nil) { if let typeId = typeId { - if self.typeDeclarations.has(typeId) { + if self.typeDeclarations.contains(typeId) { fatalError("Duplicate ID '\(typeId)' for declaration will result in bugs.") } - self.typeDeclarations.add(typeId) + self.typeDeclarations.insert(typeId) } - self.lineMarker(value: typeId, addCrossLanguageId: true) + self.lineMarker(LineMarkerOptions(value: typeId, addCrossLanguageId: true)) self.token(kind: .typeName, value: name, options: options) } - // FIXME: This! - func findNonFunctionParent(from item: DeclarationModel?) -> 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 - } - } +// // FIXME: This! +// func findNonFunctionParent(from item: DeclarationModel?) -> 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, options: ReviewTokenOptions?) {\ + func typeReference(name: String, options: ReviewTokenOptions? = nil) { var newOptions = options ?? ReviewTokenOptions() - newOptions.navigateToId = options?.navigateToId ?? "__MISSING__" + newOptions.navigateToId = options?.navigateToId ?? APIViewModel.missingId self.token(kind: .typeName, value: name, options: newOptions) } - // FIXME: This! - 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, options: ReviewTokenOptions?) { +// // FIXME: This! +// 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, options: ReviewTokenOptions? = nil) { self.token(kind: .memberName, value: name, options: options) } @@ -283,7 +285,7 @@ class APIViewModel: Tokenizable, Encodable { self.diagnostics?.append(diagnostic) } - func comment(_ text: String, options: ReviewTokenOptions?) { + func comment(_ text: String, options: ReviewTokenOptions? = nil) { var message = text if !text.starts(with: "\\") { message = "\\\\ \(message)" @@ -291,22 +293,23 @@ class APIViewModel: Tokenizable, Encodable { self.token(kind: .comment, value: message, options: options) } - func literal(_ value: String, options: ReviewTokenOptions?) { + func literal(_ value: String, options: ReviewTokenOptions? = nil) { self.token(kind: .literal, value: value, options: options) } - func stringLiteral(_ text: String, options: ReviewTokenOptions?) { - let lines = text.split("\n") + func stringLiteral(_ text: String, options: ReviewTokenOptions? = nil) { + let lines = text.split(separator: "\n") if lines.count > 1 { - self.currentLine.tokens.append(ReviewToken(kind: .stringLiteral, value: "\u0022\(text)\u0022"), options: options) + let token = ReviewToken(kind: .stringLiteral, value: "\u{0022}\(text)\u{0022}", options: options) + self.currentLine.tokens.append(token) } else { - self.punctuation("\"\"\"", options: options) + self.punctuation("\u{0022}\u{0022}\u{0022}", options: PunctuationOptions(options)) self.newline() for line in lines { - self.literal(line, options: options) + self.literal(String(line), options: options) self.newline() } - self.punctuation("\"\"\"", options: options) + self.punctuation("\u{0022}\u{0022}\u{0022}", options: PunctuationOptions(options)) } } 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/ReviewLine.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift index e1e4c3be48a..4ded29198e4 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift @@ -52,6 +52,11 @@ class ReviewLine: Tokenizable, Encodable { /// 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 + var text: String { + return tokens.map { $0.text }.joined() + } + // MARK: Codable enum CodingKeys: String, CodingKey { diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift index c19123bba0d..8d622dcfeb3 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift @@ -27,7 +27,7 @@ import Foundation /// Struct for setting reivew token options -struct ReviewTokenOptions { +public class 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? @@ -54,4 +54,66 @@ struct ReviewTokenOptions { /// Language specific style css class names var renderClasses: [String]? + + init(navigationDisplayName: String? = nil, navigateToId: String? = nil, skipDiff: Bool? = nil, isDeprecated: Bool? = nil, hasPrefixSpace: Bool? = nil, hasSuffixSpace: Bool? = nil, isDocumentation: Bool? = nil, renderClasses: [String]? = nil) { + self.navigateToId = navigateToId + self.hasPrefixSpace = hasPrefixSpace + self.hasSuffixSpace = hasSuffixSpace + self.isDeprecated = isDeprecated + self.isDocumentation = isDocumentation + self.navigationDisplayName = navigationDisplayName + self.skipDiff = skipDiff + self.renderClasses = renderClasses + } +} + +/// Struct for setting line marker options +public class LineMarkerOptions { + /// The line marker ID + var value: String? + + /// Flag to add the cross language ID + var addCrossLanguageId: Bool? + + /// Related line ID + var relatedLineId: String? + + init(value: String? = nil, addCrossLanguageId: Bool? = nil, relatedLineId: String? = nil) { + self.value = value + self.addCrossLanguageId = addCrossLanguageId + self.relatedLineId = relatedLineId + } +} + +public class PunctuationOptions: ReviewTokenOptions { + /// A string of punctuation characters you can snap to + var snapTo: String? + /// Flags that this marks the end of a context + var isContextEndLine: Bool? + + init(_ options: ReviewTokenOptions?) { + super.init() + self.navigateToId = options?.navigateToId + self.hasPrefixSpace = options?.hasPrefixSpace + self.hasSuffixSpace = options?.hasSuffixSpace + self.isDeprecated = options?.isDeprecated + self.isDocumentation = options?.isDocumentation + self.navigationDisplayName = options?.navigationDisplayName + self.skipDiff = options?.skipDiff + self.renderClasses = options?.renderClasses + } + + init(navigationDisplayName: String? = nil, navigateToId: String? = nil, skipDiff: Bool? = nil, isDeprecated: Bool? = nil, hasPrefixSpace: Bool? = nil, hasSuffixSpace: Bool? = nil, isDocumentation: Bool? = nil, renderClasses: [String]? = nil, snapTo: String? = nil, isContextEndLine: Bool? = nil) { + super.init() + self.navigateToId = navigateToId + self.hasPrefixSpace = hasPrefixSpace + self.hasSuffixSpace = hasSuffixSpace + self.isDeprecated = isDeprecated + self.isDocumentation = isDocumentation + self.navigationDisplayName = navigationDisplayName + self.skipDiff = skipDiff + self.renderClasses = renderClasses + self.snapTo = snapTo + self.isContextEndLine = isContextEndLine + } } From 430ed73a83a1309dff4f815b33369c315879fc2d Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Tue, 10 Dec 2024 13:15:35 -0800 Subject: [PATCH 04/29] Updates --- .../Sources/Models/APIViewModel.swift | 24 +++---- .../Sources/Models/DeclarationModel.swift | 26 ++++--- .../Sources/Models/ExtensionModel.swift | 13 ++-- .../Sources/Models/PackageModel.swift | 33 ++++----- .../Sources/Models/ReviewTokenOptions.swift | 65 +++++------------- .../Sources/Protocols/Linkable.swift | 2 - .../SyntaxProtocol+Extensions.swift | 68 +++++++++++++------ .../TokenKind+Extensions.swift | 4 -- 8 files changed, 111 insertions(+), 124 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift index c99f95fdd29..6ca60158883 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -70,7 +70,7 @@ class APIViewModel: Tokenizable, Encodable { private var definitionIds = Set() /// Stores the current line. All helper methods append to this. - private var currentLine: ReviewLine + internal var currentLine: ReviewLine /// Stores the parent of the current line. private var currentParent: ReviewLine? = nil @@ -199,14 +199,14 @@ class APIViewModel: Tokenizable, Encodable { } } - func punctuation(_ value: String, options: PunctuationOptions? = nil) { + func punctuation(_ value: String, options: ReviewTokenOptions? = nil, punctuationOptions: PunctuationOptions? = nil) { let token = ReviewToken(kind: .punctuation, value: value, options: options) - if let snapTo = options?.snapTo { + if let snapTo = punctuationOptions?.snapTo { self.snap(token: token, to: snapTo) } else { self.currentLine.tokens.append(token) } - if let isContextEndLine = options?.isContextEndLine { + if let isContextEndLine = punctuationOptions?.isContextEndLine { self.currentLine.isContextEndLine = true } } @@ -215,12 +215,12 @@ class APIViewModel: Tokenizable, Encodable { self.token(kind: .keyword, value: keyword, options: options) } - func lineMarker(_ options: LineMarkerOptions) { - self.currentLine.lineId = options.value ?? self.namespaceStack.value() - if options.addCrossLanguageId == true { - self.currentLine.crossLanguageId = options.value ?? self.namespaceStack.value() + func lineMarker(_ value: String? = nil, options: LineMarkerOptions? = nil) { + self.currentLine.lineId = value ?? self.namespaceStack.value() + if options?.addCrossLanguageId == true { + self.currentLine.crossLanguageId = value ?? self.namespaceStack.value() } - self.currentLine.relatedToLine = options.relatedLineId + self.currentLine.relatedToLine = options?.relatedLineId } /// Register the declaration of a new type @@ -231,7 +231,7 @@ class APIViewModel: Tokenizable, Encodable { } self.typeDeclarations.insert(typeId) } - self.lineMarker(LineMarkerOptions(value: typeId, addCrossLanguageId: true)) + self.lineMarker(typeId, options: LineMarkerOptions(addCrossLanguageId: true)) self.token(kind: .typeName, value: name, options: options) } @@ -303,13 +303,13 @@ class APIViewModel: Tokenizable, Encodable { let token = ReviewToken(kind: .stringLiteral, value: "\u{0022}\(text)\u{0022}", options: options) self.currentLine.tokens.append(token) } else { - self.punctuation("\u{0022}\u{0022}\u{0022}", options: PunctuationOptions(options)) + 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: PunctuationOptions(options)) + self.punctuation("\u{0022}\u{0022}\u{0022}", options: options) } } diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift index 43a4d775588..4bced03cbdd 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift @@ -156,7 +156,7 @@ class DeclarationModel: Tokenizable, Linkable, Equatable { let children = obj.children(viewMode: .sourceAccurate) for attr in children { let attrText = attr.withoutTrivia().description.filter { !$0.isWhitespace } - a.lineMarker(definitionId: "\(definitionId!).\(attrText)") + a.lineMarker("\(definitionId!).\(attrText)") attr.tokenize(apiview: a, parent: parent) a.newline() a.blankLines(set: 0) @@ -166,21 +166,24 @@ 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) @@ -199,11 +202,6 @@ class DeclarationModel: Tokenizable, Linkable, Equatable { } } - 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 guard let parentDecl = (parent as? DeclarationModel) else { diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift index 71943cc6ec9..236a0445078 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift @@ -132,10 +132,12 @@ class ExtensionModel: Tokenizable { func tokenize(apiview a: APIViewModel, parent: Linkable?) { for child in childNodes { + var options = ReviewTokenOptions() let childIdx = child.indexInParent 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 { @@ -145,16 +147,17 @@ class ExtensionModel: Tokenizable { } } a.newline() - a.punctuation("}", spacing: SwiftSyntax.TokenKind.rightBrace.spacing) + options.applySpacing(SwiftSyntax.TokenKind.leftBrace.spacing) + a.punctuation("}", options: options) a.newline() } else if childIdx == 7 { child.tokenize(apiview: a, parent: parent) - if var last = a.tokens.popLast() { + if var last = a.currentLine.tokens.popLast() { // These are made as type references, but they should be // type declarations - last.definitionId = self.definitionId + a.currentLine.lineId = self.definitionId last.navigateToId = self.definitionId - a.tokens.append(last) + a.currentLine.tokens.append(last) } } else { child.tokenize(apiview: a, parent: parent) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift index 27148b70de8..f0d42777e61 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift @@ -102,10 +102,13 @@ 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) + 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.newline() a.indent { for member in members { @@ -125,7 +128,8 @@ class PackageModel: Tokenizable, Linkable { } } } - a.punctuation("}", spacing: SwiftSyntax.TokenKind.rightBrace.spacing) + options.applySpacing(SwiftSyntax.TokenKind.rightBrace.spacing) + a.punctuation("}", options: options) a.newline() resolveTypeReferences(apiview: a) } @@ -174,10 +178,12 @@ class PackageModel: Tokenizable, Linkable { /// 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) + for line in a.reviewLines { + for (idx, token) in line.tokens.enumerated() { + guard token.navigateToId == APIViewModel.unresolved else { continue } + line.tokens[idx].navigateToId = a.definitionId(for: token.value!, withParent: nil) + assert (line.tokens[idx].navigateToId != APIViewModel.unresolved) + } } } @@ -196,13 +202,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/ReviewTokenOptions.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift index 8d622dcfeb3..4e25b7b2597 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift @@ -27,7 +27,7 @@ import Foundation /// Struct for setting reivew token options -public class ReviewTokenOptions { +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? @@ -55,65 +55,36 @@ public class ReviewTokenOptions { /// Language specific style css class names var renderClasses: [String]? - init(navigationDisplayName: String? = nil, navigateToId: String? = nil, skipDiff: Bool? = nil, isDeprecated: Bool? = nil, hasPrefixSpace: Bool? = nil, hasSuffixSpace: Bool? = nil, isDocumentation: Bool? = nil, renderClasses: [String]? = nil) { - self.navigateToId = navigateToId - self.hasPrefixSpace = hasPrefixSpace - self.hasSuffixSpace = hasSuffixSpace - self.isDeprecated = isDeprecated - self.isDocumentation = isDocumentation - self.navigationDisplayName = navigationDisplayName - self.skipDiff = skipDiff - self.renderClasses = renderClasses + 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 class LineMarkerOptions { - /// The line marker ID - var value: String? - +public struct LineMarkerOptions { /// Flag to add the cross language ID var addCrossLanguageId: Bool? /// Related line ID var relatedLineId: String? - - init(value: String? = nil, addCrossLanguageId: Bool? = nil, relatedLineId: String? = nil) { - self.value = value - self.addCrossLanguageId = addCrossLanguageId - self.relatedLineId = relatedLineId - } } -public class PunctuationOptions: ReviewTokenOptions { +public struct PunctuationOptions { /// A string of punctuation characters you can snap to var snapTo: String? /// Flags that this marks the end of a context var isContextEndLine: Bool? - - init(_ options: ReviewTokenOptions?) { - super.init() - self.navigateToId = options?.navigateToId - self.hasPrefixSpace = options?.hasPrefixSpace - self.hasSuffixSpace = options?.hasSuffixSpace - self.isDeprecated = options?.isDeprecated - self.isDocumentation = options?.isDocumentation - self.navigationDisplayName = options?.navigationDisplayName - self.skipDiff = options?.skipDiff - self.renderClasses = options?.renderClasses - } - - init(navigationDisplayName: String? = nil, navigateToId: String? = nil, skipDiff: Bool? = nil, isDeprecated: Bool? = nil, hasPrefixSpace: Bool? = nil, hasSuffixSpace: Bool? = nil, isDocumentation: Bool? = nil, renderClasses: [String]? = nil, snapTo: String? = nil, isContextEndLine: Bool? = nil) { - super.init() - self.navigateToId = navigateToId - self.hasPrefixSpace = hasPrefixSpace - self.hasSuffixSpace = hasSuffixSpace - self.isDeprecated = isDeprecated - self.isDocumentation = isDocumentation - self.navigationDisplayName = navigationDisplayName - self.skipDiff = skipDiff - self.renderClasses = renderClasses - self.snapTo = snapTo - self.isContextEndLine = isContextEndLine - } } 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/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift index 7d010af2c40..912290fb9a5 100644 --- a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift +++ b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift @@ -29,6 +29,7 @@ import SwiftSyntax extension SyntaxProtocol { func tokenize(apiview a: APIViewModel, parent: Linkable?) { + var options = ReviewTokenOptions() let syntaxKind = self.kind switch syntaxKind { case .associatedtypeDecl: @@ -42,14 +43,25 @@ 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.hasPrefixSpace = false + options.hasSuffixSpace = true + a.keyword(attrName, options: options) + } else { + options.hasPrefixSpace = false + options.hasSuffixSpace = false + 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.hasPrefixSpace = false + options.hasSuffixSpace = false + a.keyword("class", options: options) case .codeBlock: // Don't render code blocks. APIView is unconcerned with implementation break @@ -60,7 +72,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 +82,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.") } @@ -107,7 +120,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,15 +130,16 @@ 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 + let beforeCount = a.currentLine.tokens.count tokenizeChildren(apiview: a, parent: parent) // only render newline if tokens were actually added - if a.tokens.count > beforeCount { + if a.currentLine.tokens.count > beforeCount { a.newline() } } @@ -171,14 +185,14 @@ extension SyntaxProtocol { a.newline() if let name = PrecedenceGroupRelationSyntax(self)!.keyword { let lineId = identifier(forName: name, withPrefix: parent?.definitionId) - a.lineMarker(definitionId: lineId) + a.lineMarker(lineId) } tokenizeChildren(apiview: a, parent: parent) case .precedenceGroupAssociativity: a.newline() if let name = PrecedenceGroupAssociativitySyntax(self)!.keyword { let lineId = identifier(forName: name, withPrefix: parent?.definitionId) - a.lineMarker(definitionId: lineId) + a.lineMarker(lineId) } tokenizeChildren(apiview: a, parent: parent) case .subscriptDecl: @@ -196,7 +210,9 @@ extension SyntaxProtocol { let tokenKind = token.tokenKind let tokenText = token.withoutTrivia().description if tokenKind == .leftBrace || tokenKind == .rightBrace { - a.punctuation(tokenText, spacing: .Both) + options.hasPrefixSpace = true + options.hasSuffixSpace = true + a.punctuation(tokenText, options: options) } else { child.tokenize(token: token, apiview: a, parent: nil) } @@ -219,12 +235,14 @@ extension SyntaxProtocol { func tokenize(token: TokenSyntax, apiview a: APIViewModel, 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 +250,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 +281,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..be370d0deee 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,13 +49,11 @@ 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": From d6a463d7bac32c00315eedad4acda5e9117a3e2c Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Tue, 10 Dec 2024 13:26:20 -0800 Subject: [PATCH 05/29] Progress. --- src/swift/SwiftAPIViewCore/Sources/APIViewManager.swift | 3 +-- .../SwiftAPIViewCore/Sources/Models/APIViewModel.swift | 1 - .../SwiftAPIViewCore/Sources/Models/PackageModel.swift | 7 ++++--- src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift | 4 +++- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/APIViewManager.swift b/src/swift/SwiftAPIViewCore/Sources/APIViewManager.swift index b480d75962c..fe43b8b2a80 100644 --- a/src/swift/SwiftAPIViewCore/Sources/APIViewManager.swift +++ b/src/swift/SwiftAPIViewCore/Sources/APIViewManager.swift @@ -232,8 +232,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 = APIViewModel(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 index 6ca60158883..c9e6977e192 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -105,7 +105,6 @@ class APIViewModel: Tokenizable, Encodable { // FIXME: Actually wire this up! self.crossLanguagePackageId = nil self.tokenize(apiview: self, parent: nil) - model.navigationTokenize(apiview: self, parent: nil) } // MARK: Codable diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift index f0d42777e61..9a5efbdcc44 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift @@ -180,9 +180,10 @@ class PackageModel: Tokenizable, Linkable { func resolveTypeReferences(apiview a: APIViewModel) { for line in a.reviewLines { for (idx, token) in line.tokens.enumerated() { - guard token.navigateToId == APIViewModel.unresolved else { continue } - line.tokens[idx].navigateToId = a.definitionId(for: token.value!, withParent: nil) - assert (line.tokens[idx].navigateToId != APIViewModel.unresolved) + // FIXME: Fix this up. +// guard token.navigateToId == APIViewModel.unresolved else { continue } +// line.tokens[idx].navigateToId = a.definitionId(for: token.value!, withParent: nil) +// assert (line.tokens[idx].navigateToId != APIViewModel.unresolved) } } } diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift index 4ded29198e4..dff593ca58b 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift @@ -54,7 +54,9 @@ class ReviewLine: Tokenizable, Encodable { /// Returns the text-based representation of all tokens var text: String { - return tokens.map { $0.text }.joined() + var tokenText = tokens.map { $0.text } + tokenText.append("\n") + return tokenText.joined() } // MARK: Codable From b40ed8cd4636bf245f87c41404f6bd427f68b708 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Tue, 10 Dec 2024 14:55:42 -0800 Subject: [PATCH 06/29] Progress. --- .../Sources/Models/APIViewModel.swift | 10 +++++++--- .../Sources/Models/ReviewLine.swift | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift index c9e6977e192..3610194fa8f 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -86,7 +86,10 @@ class APIViewModel: Tokenizable, Encodable { /// Returns the text-based representation of all lines var text: String { - return reviewLines.map { $0.text }.joined() + return reviewLines.map { line in + let lineText = line.text() + return lineText + }.joined() } // MARK: Initializers @@ -312,7 +315,8 @@ class APIViewModel: Tokenizable, Encodable { } } - func indent(_ indentedLines: () -> Void) { + /// Wraps code in indentattion + func indent(_ indentedCode: () -> Void) { // ensure no trailing space at the end of the line let lastToken = self.currentLine.tokens.last lastToken?.hasSuffixSpace = false @@ -327,7 +331,7 @@ class APIViewModel: Tokenizable, Encodable { self.currentLine = ReviewLine() // handle the indented bodies - indentedLines() + indentedCode() // handle the de-indent logic guard let currentParent = self.currentParent else { diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift index dff593ca58b..7fadf46db87 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift @@ -53,10 +53,18 @@ class ReviewLine: Tokenizable, Encodable { var relatedToLine: String? /// Returns the text-based representation of all tokens - var text: String { - var tokenText = tokens.map { $0.text } - tokenText.append("\n") - return tokenText.joined() + func text(indent: Int = 0) -> String { + let indentString = String(repeating: " ", count: indent) + var tokenText = "" + for token in tokens { + tokenText += token.text + } + let childrenText = self.children?.map { $0.text(indent: indent + 2) }.joined(separator: "\n") + if let childrenText { + return "\(indentString)\(tokenText)\n\(childrenText)" + } else { + return "\(indentString)\(tokenText)" + } } // MARK: Codable From 497e520a6a5ba710a0acbd8f25eb787fc797e1ed Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Thu, 12 Dec 2024 10:29:08 -0800 Subject: [PATCH 07/29] Refactor and get children to display. --- .../Sources/Models/APIViewModel.swift | 33 +++++++++++-------- .../Sources/Models/PackageModel.swift | 1 - .../Sources/Models/ReviewLine.swift | 20 +++++++---- .../Sources/Models/ReviewToken.swift | 8 +++-- 4 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift index 3610194fa8f..ae2cf495d9c 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -147,6 +147,12 @@ class APIViewModel: Tokenizable, Encodable { func token(kind: TokenKind, value: String, options: ReviewTokenOptions? = nil) { 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) { @@ -163,8 +169,7 @@ class APIViewModel: Tokenizable, Encodable { } if let currentParent = self.currentParent { - currentParent.children = currentParent.children ?? [] - currentParent.children!.append(self.currentLine) + currentParent.children.append(self.currentLine) } else { self.reviewLines.append(self.currentLine) } @@ -177,7 +182,7 @@ class APIViewModel: Tokenizable, Encodable { var parentLines = self.currentParent?.children ?? self.reviewLines // count the number of trailing newlines var newlineCount = 0 - for line in self.reviewLines.reversed() { + for line in parentLines.reversed() { if line.tokens.count == 0 { newlineCount += 1 } else { @@ -206,10 +211,10 @@ class APIViewModel: Tokenizable, Encodable { if let snapTo = punctuationOptions?.snapTo { self.snap(token: token, to: snapTo) } else { - self.currentLine.tokens.append(token) + self.token(token) } if let isContextEndLine = punctuationOptions?.isContextEndLine { - self.currentLine.isContextEndLine = true + self.currentLine.isContextEndLine = isContextEndLine } } @@ -303,7 +308,7 @@ class APIViewModel: Tokenizable, Encodable { let lines = text.split(separator: "\n") if lines.count > 1 { let token = ReviewToken(kind: .stringLiteral, value: "\u{0022}\(text)\u{0022}", options: options) - self.currentLine.tokens.append(token) + self.token(token) } else { self.punctuation("\u{0022}\u{0022}\u{0022}", options: options) self.newline() @@ -322,7 +327,7 @@ class APIViewModel: Tokenizable, Encodable { lastToken?.hasSuffixSpace = false if let currentParent = self.currentParent { - currentParent.children?.append(self.currentLine) + currentParent.children.append(self.currentLine) self.parentStack.append(currentParent) } else { self.reviewLines.append(self.currentLine) @@ -338,9 +343,9 @@ class APIViewModel: Tokenizable, Encodable { 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 let lastChild = currentParent.children.popLast() { if (lastChild.tokens.count > 0) { - currentParent.children?.append(lastChild) + currentParent.children.append(lastChild) } } self.currentParent = self.parentStack.popLast() @@ -374,7 +379,7 @@ class APIViewModel: Tokenizable, Encodable { continue } else { // no snapping, so render in place - self.currentLine.tokens.append(target) + self.token(target) return } case .punctuation: @@ -385,12 +390,12 @@ class APIViewModel: Tokenizable, Encodable { lastLine.tokens.append(target) } else { // no snapping, so render in place - self.currentLine.tokens.append(target) + self.token(target) return } default: // no snapping, so render in place - self.currentLine.tokens.append(target) + self.token(target) return } } @@ -399,8 +404,8 @@ class APIViewModel: Tokenizable, Encodable { /// Retrieves the last line from the review func getLastLine() -> ReviewLine? { guard let currentParent = self.currentParent else { return nil } - let lastChild = currentParent.children?.last - let lastGrandchild = lastChild?.children?.last + let lastChild = currentParent.children.last + let lastGrandchild = lastChild?.children.last if let greatGrandChildren = lastGrandchild?.children { if greatGrandChildren.count > 0 { fatalError("Unexpected great-grandchild in getLastLine()!") diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift index 9a5efbdcc44..837058683d0 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift @@ -109,7 +109,6 @@ class PackageModel: Tokenizable, Linkable { a.text(name) options.applySpacing(SwiftSyntax.TokenKind.leftBrace.spacing) a.punctuation("{", options: options) - a.newline() a.indent { for member in members { member.tokenize(apiview: a, parent: self) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift index 7fadf46db87..2f4f39c2ea7 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift @@ -40,7 +40,7 @@ class ReviewLine: Tokenizable, Encodable { /// 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]? + 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, @@ -57,14 +57,18 @@ class ReviewLine: Tokenizable, Encodable { let indentString = String(repeating: " ", count: indent) var tokenText = "" for token in tokens { - tokenText += token.text + tokenText += token.text(withPreview: tokenText) } - let childrenText = self.children?.map { $0.text(indent: indent + 2) }.joined(separator: "\n") - if let childrenText { - return "\(indentString)\(tokenText)\n\(childrenText)" + let childrenText = self.children.map { $0.text(indent: indent + 2) }.joined(separator: "\n") + let value: String + if childrenText != "" { + value = "\(indentString)\(tokenText)\n\(childrenText)" + } else if tokenText != "" { + value = "\(indentString)\(tokenText)" } else { - return "\(indentString)\(tokenText)" + value = "" } + return value } // MARK: Codable @@ -84,10 +88,12 @@ class ReviewLine: Tokenizable, Encodable { try container.encode(tokens, forKey: .tokens) try container.encodeIfPresent(lineId, forKey: .lineId) try container.encodeIfPresent(crossLanguageId, forKey: .crossLanguageId) - try container.encodeIfPresent(children, forKey: .children) try container.encodeIfPresent(isHidden, forKey: .isHidden) try container.encodeIfPresent(isContextEndLine, forKey: .isContextEndLine) try container.encodeIfPresent(relatedToLine, forKey: .relatedToLine) + if (!children.isEmpty) { + try container.encode(children, forKey: .children) + } } // MARK: Tokenizable diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift index 52ff6137b68..5c2f85f81fe 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift @@ -114,8 +114,12 @@ class ReviewToken: Codable { try container.encodeIfPresent(renderClasses, forKey: .renderClasses) } - var text: String { - return value + func text(withPreview preview: String) -> String { + let previewEndsInSpace = preview.hasSuffix(" ") + let hasSuffixSpace = self.hasSuffixSpace ?? true + let suffixSpace = hasSuffixSpace ? " " : "" + let prefixSpace = (hasSuffixSpace && !previewEndsInSpace) ? " " : "" + return "\(prefixSpace)\(value)\(suffixSpace)" } } From eff6379f3f41f80174b6e5c0382929532e2ccc4d Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Thu, 12 Dec 2024 11:49:02 -0800 Subject: [PATCH 08/29] Fix issue with blankLines. --- .../SwiftAPIViewCore/Sources/Models/APIViewModel.swift | 6 +++++- .../SwiftAPIViewCore.xcodeproj/project.pbxproj | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift index ae2cf495d9c..acf89e44f2a 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -195,7 +195,11 @@ class APIViewModel: Tokenizable, Encodable { // if there are too many newlines, remove some let linesToRemove = newlineCount - count for _ in 0.. Date: Fri, 13 Dec 2024 10:55:47 -0800 Subject: [PATCH 09/29] Progress. --- src/SwiftAPIViewCore.xctestplan | 24 ++++++ .../contents.xcworkspacedata | 3 + .../Sources/Models/APIViewModel.swift | 5 ++ .../Sources/Models/PackageModel.swift | 2 + .../Sources/Models/ReviewLine.swift | 2 +- .../SyntaxProtocol+Extensions.swift | 22 +++-- .../project.pbxproj | 4 + .../xcschemes/SwiftAPIViewCore.xcscheme | 9 +- .../SwiftAPIViewCore/Tests/UtilTests.swift | 83 +++++++++++++++++++ 9 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 src/SwiftAPIViewCore.xctestplan create mode 100644 src/swift/SwiftAPIViewCore/Tests/UtilTests.swift 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/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/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift index acf89e44f2a..6589c17b44b 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -160,6 +160,8 @@ class APIViewModel: Tokenizable, Encodable { } func newline() { + let text = self.currentLine.text() + print("NEWLINE: \(text)") // ensure no trailing space at the end of the line if self.currentLine.tokens.count > 0 { let lastToken = currentLine.tokens.last @@ -178,6 +180,7 @@ class APIViewModel: Tokenizable, Encodable { /// Set the exact number of desired newlines. func blankLines(set count: Int) { + print("SET BLANKLINES: \(count)") self.newline() var parentLines = self.currentParent?.children ?? self.reviewLines // count the number of trailing newlines @@ -330,6 +333,8 @@ class APIViewModel: Tokenizable, Encodable { let lastToken = self.currentLine.tokens.last lastToken?.hasSuffixSpace = false + print("INDENT: \(self.currentLine.text())") + if let currentParent = self.currentParent { currentParent.children.append(self.currentLine) self.parentStack.append(currentParent) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift index 837058683d0..bdcb4ba241f 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift @@ -110,6 +110,7 @@ class PackageModel: Tokenizable, Linkable { 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) @@ -128,6 +129,7 @@ class PackageModel: Tokenizable, Linkable { } } options.applySpacing(SwiftSyntax.TokenKind.rightBrace.spacing) + a.blankLines(set: 0) a.punctuation("}", options: options) a.newline() resolveTypeReferences(apiview: a) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift index 2f4f39c2ea7..cac993dfedf 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift @@ -68,7 +68,7 @@ class ReviewLine: Tokenizable, Encodable { } else { value = "" } - return value + return "\(value)\n" } // MARK: Codable diff --git a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift index 912290fb9a5..74147d2ba0e 100644 --- a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift +++ b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift @@ -136,12 +136,7 @@ extension SyntaxProtocol { DeclarationModel(from: InitializerDeclSyntax(self)!, parent: parent).tokenize(apiview: a, parent: parent) case .memberDeclList: a.indent { - let beforeCount = a.currentLine.tokens.count - tokenizeChildren(apiview: a, parent: parent) - // only render newline if tokens were actually added - if a.currentLine.tokens.count > beforeCount { - a.newline() - } + tokenizeMembers(apiview: a, parent: parent) } case .memberDeclListItem: let decl = MemberDeclListItemSyntax(self)!.decl @@ -173,7 +168,6 @@ extension SyntaxProtocol { showDecl = true } if showDecl { - a.newline() tokenizeChildren(apiview: a, parent: parent) } case .precedenceGroupAttributeList: @@ -226,6 +220,20 @@ extension SyntaxProtocol { } } + func tokenizeMembers(apiview a: APIViewModel, 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) + // no blank lines for the last member, or if tokenizing didn't + // actually add anything + if (idx != lastIdx && a.currentLine.tokens.count > beforeCount) { + a.blankLines(set: 1) + } + } + } + func tokenizeChildren(apiview a: APIViewModel, parent: Linkable?) { for child in self.children(viewMode: .sourceAccurate) { child.tokenize(apiview: a, parent: parent) diff --git a/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj b/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj index a17720ccb17..575f47c5ab2 100644 --- a/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj +++ b/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 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 /* UtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A61B5292D0CADE300FC6B19 /* UtilTests.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 */; }; @@ -86,6 +87,7 @@ 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 /* UtilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilTests.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 = ""; }; @@ -210,6 +212,7 @@ B2DA7E212D0A0A6F0059E51F /* TestFiles */, B2DA7DFF2D0A0A6A0059E51F /* ExpectFiles */, 0A846A0327879D0400C967A8 /* SwiftAPIViewCoreTests.swift */, + 0A61B5292D0CADE300FC6B19 /* UtilTests.swift */, ); path = Tests; sourceTree = ""; @@ -474,6 +477,7 @@ buildActionMask = 2147483647; files = ( 0A846A2F27879E7800C967A8 /* SwiftAPIViewCoreTests.swift in Sources */, + 0A61B52A2D0CADF300FC6B19 /* UtilTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 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/UtilTests.swift b/src/swift/SwiftAPIViewCore/Tests/UtilTests.swift new file mode 100644 index 00000000000..b8de981994c --- /dev/null +++ b/src/swift/SwiftAPIViewCore/Tests/UtilTests.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 XCTest +@testable import SwiftAPIViewCore + +class UtilTests: 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 = APIViewModel(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 = APIViewModel(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)] + line2.children = [child1] + + 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" + compare(expected: expected, actual: generated) + } +} From 92849b8c90eeb3abaea798171fa50a4aaefdb448 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Fri, 13 Dec 2024 12:56:44 -0800 Subject: [PATCH 10/29] Progress. --- .../Sources/Models/APIViewModel.swift | 10 +++++---- .../Sources/Models/ReviewLine.swift | 21 +++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift index 6589c17b44b..65c4240697f 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -137,9 +137,9 @@ class APIViewModel: Tokenizable, Encodable { } func tokenize(apiview a: APIViewModel, parent: Linkable?) { - a.text("Package parsed using Swift APIView (version \(self.parserVersion))") - a.newline() - a.blankLines(set: 2) + self.text("Package parsed using Swift APIView (version \(self.parserVersion))") + self.newline() + self.blankLines(set: 2) model.tokenize(apiview: a, parent: parent) } @@ -180,7 +180,6 @@ class APIViewModel: Tokenizable, Encodable { /// Set the exact number of desired newlines. func blankLines(set count: Int) { - print("SET BLANKLINES: \(count)") self.newline() var parentLines = self.currentParent?.children ?? self.reviewLines // count the number of trailing newlines @@ -193,6 +192,7 @@ class APIViewModel: Tokenizable, Encodable { } } if newlineCount == count { + print("SET BLANKLINES: \(count). PERFECT!") return } else if (newlineCount > count) { // if there are too many newlines, remove some @@ -204,9 +204,11 @@ class APIViewModel: Tokenizable, Encodable { _ = self.reviewLines.popLast() } } + print("SET BLANKLINES: \(count). Removed \(linesToRemove).") } else { // if not enough newlines, add some let linesToAdd = count - newlineCount + print("SET BLANKLINES: \(count). Add \(linesToAdd).") for _ in 0.. String { let indentString = String(repeating: " ", count: indent) - var tokenText = "" + if tokens.count == 0 && children.count == 0 { + return "\n" + } + var value = indentString for token in tokens { - tokenText += token.text(withPreview: tokenText) + value += token.text(withPreview: value) } - let childrenText = self.children.map { $0.text(indent: indent + 2) }.joined(separator: "\n") - let value: String - if childrenText != "" { - value = "\(indentString)\(tokenText)\n\(childrenText)" - } else if tokenText != "" { - value = "\(indentString)\(tokenText)" - } else { - value = "" + let childrenLines = self.children.map { $0.text(indent: indent + 2) } + for line in childrenLines { + value += "\(line)\n" } - return "\(value)\n" + value += "\n" + return value } // MARK: Codable From 8c732752578ec96926345e67ea97be5b251b5222 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Fri, 13 Dec 2024 13:00:24 -0800 Subject: [PATCH 11/29] Temporarily disable newlines --- .../Sources/Models/DeclarationModel.swift | 11 +++++++---- .../Sources/Models/ExtensionModel.swift | 9 ++++++--- .../Sources/Models/PackageModel.swift | 18 ++++++++++++------ .../SyntaxProtocol+Extensions.swift | 12 ++++++++---- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift index 4bced03cbdd..e3a641c59de 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift @@ -158,8 +158,9 @@ class DeclarationModel: Tokenizable, Linkable, Equatable { let attrText = attr.withoutTrivia().description.filter { !$0.isWhitespace } a.lineMarker("\(definitionId!).\(attrText)") attr.tokenize(apiview: a, parent: parent) - a.newline() - a.blankLines(set: 0) + // FIXME: Newline +// a.newline() +// a.blankLines(set: 0) } case .token: if child.withoutTrivia().description == self.name { @@ -194,10 +195,12 @@ class DeclarationModel: Tokenizable, Linkable, Equatable { } } if !extensions.isEmpty { - a.blankLines(set: 1) + // FIXME: Newline + //a.blankLines(set: 1) for ext in extensions { ext.tokenize(apiview: a, parent: self) - a.blankLines(set: 1) + // FIXME: Newline + //a.blankLines(set: 1) } } } diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift index 236a0445078..bfb18901e16 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift @@ -141,15 +141,18 @@ class ExtensionModel: Tokenizable { if !members.isEmpty { a.indent { for member in members { - a.newline() + // FIXME: Newline + // a.newline() member.tokenize(apiview: a, parent: parent) } } } - a.newline() + // FIXME: Newline + //a.newline() options.applySpacing(SwiftSyntax.TokenKind.leftBrace.spacing) a.punctuation("}", options: options) - a.newline() + // FIXME: Newline + // a.newline() } else if childIdx == 7 { child.tokenize(apiview: a, parent: parent) if var last = a.currentLine.tokens.popLast() { diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift index bdcb4ba241f..f2a64d140f9 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift @@ -110,28 +110,34 @@ class PackageModel: Tokenizable, Linkable { options.applySpacing(SwiftSyntax.TokenKind.leftBrace.spacing) a.punctuation("{", options: options) a.indent { - a.blankLines(set: 0) + // FIXME: Newline + //a.blankLines(set: 0) for member in members { member.tokenize(apiview: a, parent: self) - a.blankLines(set: 1) + // FIXME: Newline + //a.blankLines(set: 1) } // render any orphaned extensions if !extensions.isEmpty { a.comment("Non-package extensions") - a.newline() + // FIXME: Newline + //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) + // FIXME: Newline + //a.blankLines(set: 1) } } } } options.applySpacing(SwiftSyntax.TokenKind.rightBrace.spacing) - a.blankLines(set: 0) + // FIXME: Newline + //a.blankLines(set: 0) a.punctuation("}", options: options) - a.newline() + // FIXME: Newline + // a.newline() resolveTypeReferences(apiview: a) } diff --git a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift index 74147d2ba0e..998e7fd583b 100644 --- a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift +++ b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift @@ -173,17 +173,20 @@ extension SyntaxProtocol { case .precedenceGroupAttributeList: a.indent { tokenizeChildren(apiview: a, parent: parent) - a.newline() + // FIXME: Newline + //a.newline() } case .precedenceGroupRelation: - a.newline() + // FIXME: Newline + //a.newline() if let name = PrecedenceGroupRelationSyntax(self)!.keyword { let lineId = identifier(forName: name, withPrefix: parent?.definitionId) a.lineMarker(lineId) } tokenizeChildren(apiview: a, parent: parent) case .precedenceGroupAssociativity: - a.newline() + // FIXME: Newline + //a.newline() if let name = PrecedenceGroupAssociativitySyntax(self)!.keyword { let lineId = identifier(forName: name, withPrefix: parent?.definitionId) a.lineMarker(lineId) @@ -229,7 +232,8 @@ extension SyntaxProtocol { // no blank lines for the last member, or if tokenizing didn't // actually add anything if (idx != lastIdx && a.currentLine.tokens.count > beforeCount) { - a.blankLines(set: 1) + // FIXME: Newline + //a.blankLines(set: 1) } } } From 4af4cc27dec9289e1901ca21de719608c9e37541 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Fri, 13 Dec 2024 13:59:30 -0800 Subject: [PATCH 12/29] Make test text work correctly for children. --- .../Sources/Models/APIViewModel.swift | 8 ++- .../Sources/Models/DeclarationModel.swift | 12 +--- .../Sources/Models/ExtensionModel.swift | 11 ++++ .../Sources/Models/PackageModel.swift | 23 ++------ .../Sources/Models/ReviewLine.swift | 9 ++- .../Sources/Models/ReviewToken.swift | 5 +- .../SyntaxProtocol+Extensions.swift | 10 ++-- .../project.pbxproj | 8 +-- .../Tests/ExpectFiles/SwiftUIExpectFile.txt | 1 + ...UtilTests.swift => TestUtilityTests.swift} | 59 +++++++++++++++++-- 10 files changed, 96 insertions(+), 50 deletions(-) rename src/swift/SwiftAPIViewCore/Tests/{UtilTests.swift => TestUtilityTests.swift} (57%) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift index 65c4240697f..39921e29534 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -86,10 +86,12 @@ class APIViewModel: Tokenizable, Encodable { /// Returns the text-based representation of all lines var text: String { - return reviewLines.map { line in + var value = "" + for line in reviewLines { let lineText = line.text() - return lineText - }.joined() + value += lineText + } + return value } // MARK: Initializers diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift index e3a641c59de..6467712f7b1 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift @@ -158,9 +158,7 @@ class DeclarationModel: Tokenizable, Linkable, Equatable { let attrText = attr.withoutTrivia().description.filter { !$0.isWhitespace } a.lineMarker("\(definitionId!).\(attrText)") attr.tokenize(apiview: a, parent: parent) - // FIXME: Newline -// a.newline() -// a.blankLines(set: 0) + a.blankLines(set: 0) } case .token: if child.withoutTrivia().description == self.name { @@ -195,13 +193,7 @@ class DeclarationModel: Tokenizable, Linkable, Equatable { } } if !extensions.isEmpty { - // FIXME: Newline - //a.blankLines(set: 1) - for ext in extensions { - ext.tokenize(apiview: a, parent: self) - // FIXME: Newline - //a.blankLines(set: 1) - } + extensions.tokenize(apiview: a, parent: self) } } diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift index bfb18901e16..3fa1ee63565 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift @@ -170,6 +170,17 @@ class ExtensionModel: Tokenizable { } extension Array { + func tokenize(apiview a: APIViewModel, 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 { diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift index f2a64d140f9..5ce00212011 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift @@ -110,34 +110,21 @@ class PackageModel: Tokenizable, Linkable { options.applySpacing(SwiftSyntax.TokenKind.leftBrace.spacing) a.punctuation("{", options: options) a.indent { - // FIXME: Newline - //a.blankLines(set: 0) + a.blankLines(set: 0) for member in members { member.tokenize(apiview: a, parent: self) - // FIXME: Newline - //a.blankLines(set: 1) + a.blankLines(set: 1) } // render any orphaned extensions if !extensions.isEmpty { a.comment("Non-package extensions") - // FIXME: Newline - //a.newline() - let endIdx = extensions.count - 1 - for (idx, ext) in extensions.enumerated() { - ext.tokenize(apiview: a, parent: nil) - if idx != endIdx { - // FIXME: Newline - //a.blankLines(set: 1) - } - } + extensions.tokenize(apiview: a, parent: nil) } } options.applySpacing(SwiftSyntax.TokenKind.rightBrace.spacing) - // FIXME: Newline - //a.blankLines(set: 0) + a.blankLines(set: 0) a.punctuation("}", options: options) - // FIXME: Newline - // a.newline() + a.newline() resolveTypeReferences(apiview: a) } diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift index b722b0470dd..05560bba28b 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift @@ -54,6 +54,7 @@ class ReviewLine: Tokenizable, Encodable { /// 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" @@ -62,11 +63,13 @@ class ReviewLine: Tokenizable, Encodable { for token in tokens { value += token.text(withPreview: value) } - let childrenLines = self.children.map { $0.text(indent: indent + 2) } + if tokens.count > 0 { + value += "\n" + } + let childrenLines = self.children.map { $0.text(indent: indent + indentCount) } for line in childrenLines { - value += "\(line)\n" + value += line } - value += "\n" return value } diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift index 5c2f85f81fe..2974789adfc 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift @@ -116,9 +116,10 @@ class ReviewToken: Codable { func text(withPreview preview: String) -> String { let previewEndsInSpace = preview.hasSuffix(" ") - let hasSuffixSpace = self.hasSuffixSpace ?? true + let hasSuffixSpace = self.hasSuffixSpace != nil ? self.hasSuffixSpace! : true + let hasPrefixSpace = self.hasPrefixSpace != nil ? self.hasPrefixSpace! : false let suffixSpace = hasSuffixSpace ? " " : "" - let prefixSpace = (hasSuffixSpace && !previewEndsInSpace) ? " " : "" + let prefixSpace = (hasPrefixSpace && !previewEndsInSpace) ? " " : "" return "\(prefixSpace)\(value)\(suffixSpace)" } } diff --git a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift index 998e7fd583b..69d6198b4c1 100644 --- a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift +++ b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift @@ -229,12 +229,10 @@ extension SyntaxProtocol { for (idx, child) in children.enumerated() { let beforeCount = a.currentLine.tokens.count child.tokenize(apiview: a, parent: parent) - // no blank lines for the last member, or if tokenizing didn't - // actually add anything - if (idx != lastIdx && a.currentLine.tokens.count > beforeCount) { - // FIXME: Newline - //a.blankLines(set: 1) - } + // skip if no tokens were actually added + guard (a.currentLine.tokens.count > beforeCount) else { continue } + // add 1 blank line, except for the last member + a.blankLines(set: idx != lastIdx ? 1 : 0) } } diff --git a/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj b/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj index 575f47c5ab2..f9ca60452fc 100644 --- a/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj +++ b/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj @@ -12,7 +12,7 @@ 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 /* UtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A61B5292D0CADE300FC6B19 /* UtilTests.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 */; }; @@ -87,7 +87,7 @@ 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 /* UtilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilTests.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 = ""; }; @@ -212,7 +212,7 @@ B2DA7E212D0A0A6F0059E51F /* TestFiles */, B2DA7DFF2D0A0A6A0059E51F /* ExpectFiles */, 0A846A0327879D0400C967A8 /* SwiftAPIViewCoreTests.swift */, - 0A61B5292D0CADE300FC6B19 /* UtilTests.swift */, + 0A61B5292D0CADE300FC6B19 /* TestUtilityTests.swift */, ); path = Tests; sourceTree = ""; @@ -477,7 +477,7 @@ buildActionMask = 2147483647; files = ( 0A846A2F27879E7800C967A8 /* SwiftAPIViewCoreTests.swift in Sources */, - 0A61B52A2D0CADF300FC6B19 /* UtilTests.swift in Sources */, + 0A61B52A2D0CADF300FC6B19 /* TestUtilityTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/SwiftUIExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/SwiftUIExpectFile.txt index ec87bba046b..93dd05d56ba 100644 --- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/SwiftUIExpectFile.txt +++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/SwiftUIExpectFile.txt @@ -4,6 +4,7 @@ Package parsed using Swift APIView (version 0.2.2) 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/UtilTests.swift b/src/swift/SwiftAPIViewCore/Tests/TestUtilityTests.swift similarity index 57% rename from src/swift/SwiftAPIViewCore/Tests/UtilTests.swift rename to src/swift/SwiftAPIViewCore/Tests/TestUtilityTests.swift index b8de981994c..0507abc374b 100644 --- a/src/swift/SwiftAPIViewCore/Tests/UtilTests.swift +++ b/src/swift/SwiftAPIViewCore/Tests/TestUtilityTests.swift @@ -27,7 +27,7 @@ import XCTest @testable import SwiftAPIViewCore -class UtilTests: XCTestCase { +class TestUtilityTests: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() @@ -73,11 +73,62 @@ class UtilTests: XCTestCase { 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)] - line2.children = [child1] - + 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" + 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 = APIViewModel(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 = APIViewModel(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) } } From c54a688ca4a2868b53de79033cc1e18154af3cd1 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Mon, 16 Dec 2024 08:04:06 -0800 Subject: [PATCH 13/29] Progress on syntax --- .../Sources/Models/APIViewModel.swift | 22 ++++++++----------- .../Sources/Models/DeclarationModel.swift | 1 - .../Sources/Models/ExtensionModel.swift | 6 +---- .../Sources/Models/PackageModel.swift | 2 +- .../Sources/Models/ReviewTokenOptions.swift | 7 ------ .../SyntaxProtocol+Extensions.swift | 18 +++++++-------- .../TokenKind+Extensions.swift | 2 +- 7 files changed, 20 insertions(+), 38 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift index 39921e29534..798f81f7682 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -147,6 +147,9 @@ class APIViewModel: Tokenizable, Encodable { // 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 @@ -163,7 +166,6 @@ class APIViewModel: Tokenizable, Encodable { func newline() { let text = self.currentLine.text() - print("NEWLINE: \(text)") // ensure no trailing space at the end of the line if self.currentLine.tokens.count > 0 { let lastToken = currentLine.tokens.last @@ -194,7 +196,6 @@ class APIViewModel: Tokenizable, Encodable { } } if newlineCount == count { - print("SET BLANKLINES: \(count). PERFECT!") return } else if (newlineCount > count) { // if there are too many newlines, remove some @@ -206,27 +207,23 @@ class APIViewModel: Tokenizable, Encodable { _ = self.reviewLines.popLast() } } - print("SET BLANKLINES: \(count). Removed \(linesToRemove).") } else { // if not enough newlines, add some let linesToAdd = count - newlineCount - print("SET BLANKLINES: \(count). Add \(linesToAdd).") for _ in 0.. ReviewLine? { - guard let currentParent = self.currentParent else { return nil } + guard let currentParent = self.currentParent ?? self.reviewLines.last else { return nil } let lastChild = currentParent.children.last let lastGrandchild = lastChild?.children.last if let greatGrandChildren = lastGrandchild?.children { diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift index 6467712f7b1..b71435e3233 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift @@ -167,7 +167,6 @@ class DeclarationModel: Tokenizable, Linkable, Equatable { case .class: a.typeDeclaration(name: name, typeId: definitionId) case .enum: - a.typeDeclaration(name: name, typeId: definitionId) case .method: a.lineMarker(definitionId) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift index 3fa1ee63565..a44ac5b83d9 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift @@ -147,15 +147,11 @@ class ExtensionModel: Tokenizable { } } } - // FIXME: Newline - //a.newline() options.applySpacing(SwiftSyntax.TokenKind.leftBrace.spacing) a.punctuation("}", options: options) - // FIXME: Newline - // a.newline() } else if childIdx == 7 { child.tokenize(apiview: a, parent: parent) - if var last = a.currentLine.tokens.popLast() { + if let last = a.currentLine.tokens.popLast() { // These are made as type references, but they should be // type declarations a.currentLine.lineId = self.definitionId diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift index 5ce00212011..46f20d21e04 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift @@ -121,8 +121,8 @@ class PackageModel: Tokenizable, Linkable { extensions.tokenize(apiview: a, parent: nil) } } - options.applySpacing(SwiftSyntax.TokenKind.rightBrace.spacing) a.blankLines(set: 0) + options.applySpacing(SwiftSyntax.TokenKind.rightBrace.spacing) a.punctuation("}", options: options) a.newline() resolveTypeReferences(apiview: a) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift index 4e25b7b2597..912110eb131 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift @@ -81,10 +81,3 @@ public struct LineMarkerOptions { /// Related line ID var relatedLineId: String? } - -public struct PunctuationOptions { - /// A string of punctuation characters you can snap to - var snapTo: String? - /// Flags that this marks the end of a context - var isContextEndLine: Bool? -} diff --git a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift index 69d6198b4c1..a7ab8d582eb 100644 --- a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift +++ b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift @@ -44,12 +44,10 @@ extension SyntaxProtocol { let attrName = child.withoutTrivia().description // don't add space if the attribute has parameters if children.count == 2 { - options.hasPrefixSpace = false - options.hasSuffixSpace = true + options.applySpacing(.Trailing) a.keyword(attrName, options: options) } else { - options.hasPrefixSpace = false - options.hasSuffixSpace = false + options.applySpacing(.Neither) a.keyword(attrName, options: options) } @@ -59,8 +57,7 @@ extension SyntaxProtocol { } case .classRestrictionType: // in this simple context, class should not have a trailing space - options.hasPrefixSpace = false - options.hasSuffixSpace = false + options.applySpacing(.Neither) a.keyword("class", options: options) case .codeBlock: // Don't render code blocks. APIView is unconcerned with implementation @@ -107,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 { @@ -207,8 +205,7 @@ extension SyntaxProtocol { let tokenKind = token.tokenKind let tokenText = token.withoutTrivia().description if tokenKind == .leftBrace || tokenKind == .rightBrace { - options.hasPrefixSpace = true - options.hasSuffixSpace = true + options.applySpacing(tokenKind.spacing) a.punctuation(tokenText, options: options) } else { child.tokenize(token: token, apiview: a, parent: nil) @@ -231,8 +228,9 @@ extension SyntaxProtocol { child.tokenize(apiview: a, parent: parent) // skip if no tokens were actually added guard (a.currentLine.tokens.count > beforeCount) else { continue } + // FIXME: Only add a blank line between members if there's attributes // add 1 blank line, except for the last member - a.blankLines(set: idx != lastIdx ? 1 : 0) + a.blankLines(set: idx != lastIdx ? 0 : 0) } } diff --git a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/TokenKind+Extensions.swift b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/TokenKind+Extensions.swift index be370d0deee..a7cd8bb7aa1 100644 --- a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/TokenKind+Extensions.swift +++ b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/TokenKind+Extensions.swift @@ -57,7 +57,7 @@ extension SwiftSyntax.TokenKind { 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: From c1db006b0dadffbf46511784814ed452c7e14e5d Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Wed, 18 Dec 2024 08:56:41 -0600 Subject: [PATCH 14/29] Progress on tests. --- .../Sources/Models/APIViewModel.swift | 13 +++++-------- .../Sources/Models/DeclarationModel.swift | 15 ++++++++++++++- .../Sources/Models/ExtensionModel.swift | 4 ++-- .../SyntaxProtocol+Extensions.swift | 16 +++------------- .../Tests/ExpectFiles/ExtensionExpectFile.txt | 1 + .../ExpectFiles/PrivateInternalExpectFile.txt | 3 +-- .../Tests/ExpectFiles/ProtocolExpectFile.txt | 1 + 7 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift index 798f81f7682..1b3a407ab10 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -146,7 +146,7 @@ class APIViewModel: Tokenizable, Encodable { } // MARK: Token Emitters - func token(kind: TokenKind, value: String, options: ReviewTokenOptions? = nil) { + 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 @@ -413,13 +413,10 @@ class APIViewModel: Tokenizable, Encodable { /// Retrieves the last line from the review func getLastLine() -> ReviewLine? { guard let currentParent = self.currentParent ?? self.reviewLines.last else { return nil } - let lastChild = currentParent.children.last - let lastGrandchild = lastChild?.children.last - if let greatGrandChildren = lastGrandchild?.children { - if greatGrandChildren.count > 0 { - fatalError("Unexpected great-grandchild in getLastLine()!") - } + var current = currentParent.children.last + if let lastChild = current?.children.last { + current = lastChild } - return lastGrandchild ?? lastChild + return current } } diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift index b71435e3233..cecd1536de7 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) } @@ -148,9 +149,13 @@ class DeclarationModel: Tokenizable, Linkable, Equatable { } func tokenize(apiview a: APIViewModel, parent: Linkable?) { - for child in childNodes { + for (idx, child) in childNodes.enumerated() { switch child.kind { case .attributeList: + let lastToken = a.getLastLine()?.tokens.last + // Ensure declarations that have an attributeList have a blank line, + // unless it is the first child + a.blankLines(set: lastToken?.value == "{" ? 0 : 1) // attributes on declarations should have newlines let obj = AttributeListSyntax(child)! let children = obj.children(viewMode: .sourceAccurate) @@ -160,6 +165,14 @@ class DeclarationModel: Tokenizable, Linkable, Equatable { attr.tokenize(apiview: a, parent: parent) 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: if child.withoutTrivia().description == self.name { // render as the correct APIView token type diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift index a44ac5b83d9..ab846e14100 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift @@ -141,14 +141,14 @@ class ExtensionModel: Tokenizable { if !members.isEmpty { a.indent { for member in members { - // FIXME: Newline - // a.newline() member.tokenize(apiview: a, parent: parent) + a.blankLines(set: 0) } } } options.applySpacing(SwiftSyntax.TokenKind.leftBrace.spacing) a.punctuation("}", options: options) + a.newline() } else if childIdx == 7 { child.tokenize(apiview: a, parent: parent) if let last = a.currentLine.tokens.popLast() { diff --git a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift index a7ab8d582eb..02d2d2aefa5 100644 --- a/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift +++ b/src/swift/SwiftAPIViewCore/Sources/SwiftSyntax+Extensions/SyntaxProtocol+Extensions.swift @@ -168,23 +168,15 @@ extension SyntaxProtocol { if showDecl { tokenizeChildren(apiview: a, parent: parent) } - case .precedenceGroupAttributeList: - a.indent { - tokenizeChildren(apiview: a, parent: parent) - // FIXME: Newline - //a.newline() - } case .precedenceGroupRelation: - // FIXME: Newline - //a.newline() + a.blankLines(set: 0) if let name = PrecedenceGroupRelationSyntax(self)!.keyword { let lineId = identifier(forName: name, withPrefix: parent?.definitionId) a.lineMarker(lineId) } tokenizeChildren(apiview: a, parent: parent) case .precedenceGroupAssociativity: - // FIXME: Newline - //a.newline() + a.blankLines(set: 0) if let name = PrecedenceGroupAssociativitySyntax(self)!.keyword { let lineId = identifier(forName: name, withPrefix: parent?.definitionId) a.lineMarker(lineId) @@ -228,9 +220,7 @@ extension SyntaxProtocol { child.tokenize(apiview: a, parent: parent) // skip if no tokens were actually added guard (a.currentLine.tokens.count > beforeCount) else { continue } - // FIXME: Only add a blank line between members if there's attributes - // add 1 blank line, except for the last member - a.blankLines(set: idx != lastIdx ? 0 : 0) + a.blankLines(set: 0) } } diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ExtensionExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ExtensionExpectFile.txt index 761c119f743..8d1b5d614f3 100644 --- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ExtensionExpectFile.txt +++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ExtensionExpectFile.txt @@ -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/PrivateInternalExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/PrivateInternalExpectFile.txt index f3f4d9ffb29..6b47b2fa5b2 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 PrivateInternalTestFile.swifttxt { -} +package PrivateInternalTestFile.swifttxt {} diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ProtocolExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ProtocolExpectFile.txt index 644808185e5..b3f3a977cbe 100644 --- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ProtocolExpectFile.txt +++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ProtocolExpectFile.txt @@ -110,6 +110,7 @@ package ProtocolTestFile.swifttxt { public func wishHappyBirthday(to: Named & Aged) \\ Non-package extensions + extension Array: TextRepresentable where Element: TextRepresentable { public var textualDescription: String } From a387df03258e3423f20418db4cca9390a4c01db8 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Wed, 18 Dec 2024 12:00:13 -0600 Subject: [PATCH 15/29] Syntax progress. --- .../Sources/Models/APIViewModel.swift | 4 +-- .../Sources/Models/DeclarationModel.swift | 3 +-- .../Sources/Models/ExtensionModel.swift | 26 ++++++++++--------- .../Sources/Models/PackageModel.swift | 2 +- .../Tests/ExpectFiles/ProtocolExpectFile.txt | 3 +-- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift index 1b3a407ab10..a533ce7a2b8 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -73,7 +73,7 @@ class APIViewModel: Tokenizable, Encodable { internal var currentLine: ReviewLine /// Stores the parent of the current line. - private var currentParent: ReviewLine? = nil + internal var currentParent: ReviewLine? = nil /// Stores the stack of parent lines private var parentStack: [ReviewLine] = [] @@ -146,7 +146,7 @@ class APIViewModel: Tokenizable, Encodable { } // MARK: Token Emitters - func token(kind: TokenKind, value: String, options: ReviewTokenOptions? = nil) { + 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 diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift index cecd1536de7..d94156409d0 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift @@ -152,10 +152,9 @@ class DeclarationModel: Tokenizable, Linkable, Equatable { for (idx, child) in childNodes.enumerated() { switch child.kind { case .attributeList: - let lastToken = a.getLastLine()?.tokens.last // Ensure declarations that have an attributeList have a blank line, // unless it is the first child - a.blankLines(set: lastToken?.value == "{" ? 0 : 1) + 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) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift index ab846e14100..73a7c44ede0 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift @@ -131,10 +131,20 @@ class ExtensionModel: Tokenizable { } func tokenize(apiview a: APIViewModel, parent: Linkable?) { - for child in childNodes { + for (idx, child) in childNodes.enumerated() { var options = ReviewTokenOptions() let childIdx = child.indexInParent - if childIdx == 13 { + print("\(childIdx) \(idx) \(child.kind)") + 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 options.applySpacing(SwiftSyntax.TokenKind.leftBrace.spacing) a.punctuation("{", options: options) @@ -146,18 +156,10 @@ class ExtensionModel: Tokenizable { } } } - options.applySpacing(SwiftSyntax.TokenKind.leftBrace.spacing) + a.blankLines(set: 0) + options.applySpacing(SwiftSyntax.TokenKind.rightBrace.spacing) a.punctuation("}", options: options) a.newline() - } else 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 { child.tokenize(apiview: a, parent: parent) } diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift index 46f20d21e04..ceeab62262a 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift @@ -124,8 +124,8 @@ class PackageModel: Tokenizable, Linkable { a.blankLines(set: 0) options.applySpacing(SwiftSyntax.TokenKind.rightBrace.spacing) a.punctuation("}", options: options) - a.newline() resolveTypeReferences(apiview: a) + a.blankLines(set: 0) } /// Move extensions into the model representations for declared package types diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ProtocolExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ProtocolExpectFile.txt index b3f3a977cbe..ebbfa218cdb 100644 --- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ProtocolExpectFile.txt +++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/ProtocolExpectFile.txt @@ -38,8 +38,7 @@ package ProtocolTestFile.swifttxt { public var textualDescription: String } - extension Hamster: TextRepresentable { - } + extension Hamster: TextRepresentable {} public protocol Named { var name: String { get } From 8b00e8e247c1657212d168ea6bcdcec17c174470 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Wed, 18 Dec 2024 12:24:45 -0600 Subject: [PATCH 16/29] All syntax but one! --- .../SwiftAPIViewCore/Sources/Models/APIViewModel.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift index a533ce7a2b8..2ee0b182ef4 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -221,6 +221,15 @@ class APIViewModel: Tokenizable, Encodable { if value == "}" { self.snap(token: token, to: "{") self.currentLine.isContextEndLine = true + } else if value == "(" { + // Remove suffix space if preceding token is text + if let lastToken = currentLine.tokens.last { + let shorten = ["get", "set", "willSet", "didSet"] + if shorten.contains(lastToken.value) { + lastToken.hasSuffixSpace = false + } + } + self.token(token) } else { self.token(token) } From 202a5f9b667a5f6a1ce384a48301de621421cb9a Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Wed, 18 Dec 2024 12:46:40 -0600 Subject: [PATCH 17/29] Finally all tests pass syntactically! --- .../SwiftAPIViewCore/Sources/Models/APIViewModel.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift index 2ee0b182ef4..6470c2e16a3 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift @@ -421,8 +421,12 @@ class APIViewModel: Tokenizable, Encodable { /// Retrieves the last line from the review func getLastLine() -> ReviewLine? { - guard let currentParent = self.currentParent ?? self.reviewLines.last else { return nil } - var current = currentParent.children.last + 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 } From bc5264e3eea50d8de5eeca64e5a412e76c5a503c Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Thu, 19 Dec 2024 11:25:56 -0600 Subject: [PATCH 18/29] Add lineId and relatedLineId checking to tests. --- .../Sources/APIViewManager.swift | 10 +- .../Sources/Models/ExtensionModel.swift | 1 - .../Tests/SwiftAPIViewCoreTests.swift | 123 ++++++++++++++++++ .../Sources/SwiftAPIViewTests.h | 30 ++--- 4 files changed, 144 insertions(+), 20 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/APIViewManager.swift b/src/swift/SwiftAPIViewCore/Sources/APIViewManager.swift index fe43b8b2a80..f3b97f9317d 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: APIViewModel? + var statements = OrderedDictionary() // MARK: Initializer public init(mode: APIViewManagerMode = .commandLine) { self.mode = mode + self.model = nil super.init(viewMode: .all) } @@ -84,14 +87,13 @@ 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 } } diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift index 73a7c44ede0..0bb3163d08e 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift @@ -134,7 +134,6 @@ class ExtensionModel: Tokenizable { for (idx, child) in childNodes.enumerated() { var options = ReviewTokenOptions() let childIdx = child.indexInParent - print("\(childIdx) \(idx) \(child.kind)") if childIdx == 7 { child.tokenize(apiview: a, parent: parent) if let last = a.currentLine.tokens.popLast() { diff --git a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift index e34345db9d7..43b65e88e42 100644 --- a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift +++ b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift @@ -29,6 +29,14 @@ 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; + } + override func setUpWithError() throws { try super.setUpWithError() continueAfterFailure = false @@ -50,6 +58,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 +72,88 @@ 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: APIViewModel) { + + func validate(line: ReviewLine) { + // ensure there are no repeated definition IDs + if let lineId = line.lineId { + XCTAssertFalse(lineIds.contains(lineId), "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: APIViewModel) -> [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 let isEndContext = line.isContextEndLine { + 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 + } + } + } + } + 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 + } + XCTAssertEqual(lhs, rhs) + } + + // MARK: Tests + func testAttributes() throws { let manager = APIViewManager(mode: .testing) manager.config.sourcePath = pathFor(testFile: "AttributesTestFile") @@ -70,6 +161,18 @@ 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.MyRenamedProtocol": ReviewLineData(relatedToCount: 0, 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 +182,8 @@ 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!) } func testExtensions() throws { @@ -88,6 +193,8 @@ 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!) } func testFunctions() throws { @@ -97,6 +204,8 @@ 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!) } func testGenerics() throws { @@ -106,6 +215,8 @@ 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!) } func testInitializers() throws { @@ -115,6 +226,8 @@ 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!) } func testOperators() throws { @@ -124,6 +237,8 @@ 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!) } func testPrivateInternal() throws { @@ -133,6 +248,8 @@ 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!) } func testProperties() throws { @@ -142,6 +259,8 @@ 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!) } func testProtocols() throws { @@ -151,6 +270,8 @@ 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!) } func testSwiftUI() throws { @@ -160,5 +281,7 @@ 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!) } } diff --git a/src/swift/SwiftAPIViewTests/Sources/SwiftAPIViewTests.h b/src/swift/SwiftAPIViewTests/Sources/SwiftAPIViewTests.h index 6d4116f12b3..9c8e00bf6f6 100644 --- a/src/swift/SwiftAPIViewTests/Sources/SwiftAPIViewTests.h +++ b/src/swift/SwiftAPIViewTests/Sources/SwiftAPIViewTests.h @@ -1,18 +1,18 @@ +//// +//// SwiftAPIViewTests.h +//// SwiftAPIViewTests +//// +//// Created by Travis Prescott on 3/30/22. +//// // -// SwiftAPIViewTests.h -// SwiftAPIViewTests +//#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 // -// 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 - - From d853825fabf84af5df6139f98b9411410888cefd Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Thu, 19 Dec 2024 11:30:30 -0600 Subject: [PATCH 19/29] Rename APIViewModel to CodeModel --- .../SwiftAPIViewCore/Sources/APIViewManager.swift | 8 ++++---- .../Sources/Models/CodeDiagnostic.swift | 2 +- .../Models/{APIViewModel.swift => CodeModel.swift} | 6 +++--- .../Sources/Models/DeclarationModel.swift | 4 ++-- .../Sources/Models/ExtensionModel.swift | 6 +++--- .../Sources/Models/PackageModel.swift | 8 ++++---- .../SwiftAPIViewCore/Sources/Models/ReviewLine.swift | 2 +- .../Sources/Protocols/Tokenizable.swift | 2 +- .../SyntaxProtocol+Extensions.swift | 12 ++++++------ .../SwiftAPIViewCore.xcodeproj/project.pbxproj | 8 ++++---- .../Tests/SwiftAPIViewCoreTests.swift | 2 +- 11 files changed, 30 insertions(+), 30 deletions(-) rename src/swift/SwiftAPIViewCore/Sources/Models/{APIViewModel.swift => CodeModel.swift} (98%) diff --git a/src/swift/SwiftAPIViewCore/Sources/APIViewManager.swift b/src/swift/SwiftAPIViewCore/Sources/APIViewManager.swift index f3b97f9317d..a29874310bc 100644 --- a/src/swift/SwiftAPIViewCore/Sources/APIViewManager.swift +++ b/src/swift/SwiftAPIViewCore/Sources/APIViewManager.swift @@ -66,7 +66,7 @@ public class APIViewManager: SyntaxVisitor { var mode: APIViewManagerMode - var model: APIViewModel? + var model: CodeModel? var statements = OrderedDictionary() @@ -98,7 +98,7 @@ public class APIViewManager: SyntaxVisitor { } /// 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) @@ -191,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? @@ -234,7 +234,7 @@ public class APIViewManager: SyntaxVisitor { } config.packageName = packageName! config.packageVersion = packageVersion! - let apiView = APIViewModel(packageName: packageName!, packageVersion: 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/CodeDiagnostic.swift b/src/swift/SwiftAPIViewCore/Sources/Models/CodeDiagnostic.swift index a86aac05844..edd203a987c 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/CodeDiagnostic.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/CodeDiagnostic.swift @@ -76,7 +76,7 @@ class CodeDiagnostic: Tokenizable, Encodable { // MARK: Tokenizable - func tokenize(apiview: APIViewModel, parent: (any Linkable)?) { + func tokenize(apiview: CodeModel, parent: (any Linkable)?) { fatalError("Not implemented!") } } diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift similarity index 98% rename from src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift rename to src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift index 6470c2e16a3..a75d670b6c1 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/APIViewModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift @@ -28,7 +28,7 @@ import Foundation import SwiftSyntax -class APIViewModel: Tokenizable, Encodable { +class CodeModel: Tokenizable, Encodable { /// The package name used by APIView var packageName: String @@ -138,7 +138,7 @@ class APIViewModel: Tokenizable, Encodable { try container.encodeIfPresent(diagnostics, forKey: .diagnostics) } - func tokenize(apiview a: APIViewModel, parent: Linkable?) { + func tokenize(apiview a: CodeModel, parent: Linkable?) { self.text("Package parsed using Swift APIView (version \(self.parserVersion))") self.newline() self.blankLines(set: 2) @@ -274,7 +274,7 @@ class APIViewModel: Tokenizable, Encodable { /// Link to a registered type func typeReference(name: String, options: ReviewTokenOptions? = nil) { var newOptions = options ?? ReviewTokenOptions() - newOptions.navigateToId = options?.navigateToId ?? APIViewModel.missingId + newOptions.navigateToId = options?.navigateToId ?? CodeModel.missingId self.token(kind: .typeName, value: name, options: newOptions) } diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift index d94156409d0..6b1143c386f 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift @@ -148,7 +148,7 @@ class DeclarationModel: Tokenizable, Linkable, Equatable { self.extensions = [] } - func tokenize(apiview a: APIViewModel, parent: Linkable?) { + func tokenize(apiview a: CodeModel, parent: Linkable?) { for (idx, child) in childNodes.enumerated() { switch child.kind { case .attributeList: @@ -209,7 +209,7 @@ class DeclarationModel: Tokenizable, Linkable, Equatable { } 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 0bb3163d08e..3b276055a32 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift @@ -124,13 +124,13 @@ class ExtensionModel: Tokenizable { } 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 (idx, child) in childNodes.enumerated() { var options = ReviewTokenOptions() let childIdx = child.indexInParent @@ -167,7 +167,7 @@ class ExtensionModel: Tokenizable { } extension Array { - func tokenize(apiview a: APIViewModel, parent: Linkable?) { + func tokenize(apiview a: CodeModel, parent: Linkable?) { a.blankLines(set: 1) let lastIdx = self.count - 1 for (idx, ext) in self.enumerated() { diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift index ceeab62262a..533d53eb19f 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift @@ -101,7 +101,7 @@ class PackageModel: Tokenizable, Linkable { } } - func tokenize(apiview a: APIViewModel, parent: Linkable?) { + func tokenize(apiview a: CodeModel, parent: Linkable?) { var options = ReviewTokenOptions() options.hasSuffixSpace = true a.text("package", options: options) @@ -171,13 +171,13 @@ class PackageModel: Tokenizable, Linkable { } /// attempt to resolve type references that are declared after they are used - func resolveTypeReferences(apiview a: APIViewModel) { + func resolveTypeReferences(apiview a: CodeModel) { for line in a.reviewLines { for (idx, token) in line.tokens.enumerated() { // FIXME: Fix this up. -// guard token.navigateToId == APIViewModel.unresolved else { continue } +// guard token.navigateToId == CodeModel.unresolved else { continue } // line.tokens[idx].navigateToId = a.definitionId(for: token.value!, withParent: nil) -// assert (line.tokens[idx].navigateToId != APIViewModel.unresolved) +// assert (line.tokens[idx].navigateToId != CodeModel.unresolved) } } } diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift index 05560bba28b..01b7e38f634 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift @@ -100,7 +100,7 @@ class ReviewLine: Tokenizable, Encodable { // MARK: Tokenizable - func tokenize(apiview: APIViewModel, parent: (any Linkable)?) { + func tokenize(apiview: CodeModel, parent: (any Linkable)?) { fatalError("Not implemented!") } } 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 02d2d2aefa5..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,7 @@ 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 { @@ -138,7 +138,7 @@ extension SyntaxProtocol { } 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: @@ -158,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 @@ -212,7 +212,7 @@ extension SyntaxProtocol { } } - func tokenizeMembers(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() { @@ -224,13 +224,13 @@ extension SyntaxProtocol { } } - func tokenizeChildren(apiview a: APIViewModel, parent: Linkable?) { + 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() diff --git a/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj b/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj index f9ca60452fc..e067d2e784a 100644 --- a/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj +++ b/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj @@ -18,7 +18,7 @@ 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 */; }; 0A6C6593292DA0140075C56F /* Linkable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A84076927EE4E2C00801E60 /* Linkable.swift */; }; @@ -96,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; }; @@ -244,8 +244,8 @@ isa = PBXGroup; children = ( 0A76BF94294BA10A007C776E /* AccessLevel.swift */, - 0A84076527EE3DCE00801E60 /* APIViewModel.swift */, 0A61B51F2D0B764600FC6B19 /* CodeDiagnostic.swift */, + 0A84076527EE3DCE00801E60 /* CodeModel.swift */, 0A76BF90294B940A007C776E /* DeclarationModel.swift */, 0AA1BFBD2953839E00AE8C11 /* ExtensionModel.swift */, 0A61B5252D0B76C400FC6B19 /* NamespaceStack.swift */, @@ -449,7 +449,7 @@ 0AA1BFBE2953839E00AE8C11 /* ExtensionModel.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 */, diff --git a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift index 43b65e88e42..1b0108cdd6f 100644 --- a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift +++ b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift @@ -111,7 +111,7 @@ class SwiftAPIViewCoreTests: XCTestCase { subMap.relatedToCount += 1 mainMap[related] = subMap } - if let isEndContext = line.isContextEndLine { + if let _ = line.isContextEndLine { guard lastKey != nil else { XCTFail("isEndContext found without a related line.") return nil From 3a053d9bcaa03911f37efb6f5507edd7afa19c0d Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Thu, 19 Dec 2024 11:38:30 -0600 Subject: [PATCH 20/29] Add auto-navigation. --- src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift | 4 ++++ .../SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift | 4 ++-- src/swift/SwiftAPIViewCore/Tests/TestUtilityTests.swift | 8 ++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift index a75d670b6c1..dd58382f238 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift @@ -256,6 +256,9 @@ class CodeModel: Tokenizable, Encodable { self.typeDeclarations.insert(typeId) } self.lineMarker(typeId, options: LineMarkerOptions(addCrossLanguageId: true)) + var newOptions = options ?? ReviewTokenOptions() + newOptions.navigationDisplayName = options?.navigationDisplayName ?? name + newOptions.navigateToId = options?.navigateToId ?? typeId self.token(kind: .typeName, value: name, options: options) } @@ -275,6 +278,7 @@ class CodeModel: Tokenizable, Encodable { func typeReference(name: String, options: ReviewTokenOptions? = nil) { var newOptions = options ?? ReviewTokenOptions() newOptions.navigateToId = options?.navigateToId ?? CodeModel.missingId + newOptions.navigationDisplayName = options?.navigationDisplayName ?? name self.token(kind: .typeName, value: name, options: newOptions) } diff --git a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift index 1b0108cdd6f..6c20807a9bb 100644 --- a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift +++ b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift @@ -74,7 +74,7 @@ class SwiftAPIViewCoreTests: XCTestCase { /// Ensure there are no duplicate line IDs in the review, as that would lead /// to functional bugs on the web. - private func validateLineIds(apiview: APIViewModel) { + private func validateLineIds(apiview: CodeModel) { func validate(line: ReviewLine) { // ensure there are no repeated definition IDs @@ -96,7 +96,7 @@ class SwiftAPIViewCoreTests: XCTestCase { } /// Extracts related lines from the APIView to ensure proper collapsing behavior on the web. - private func getRelatedLineMetadata(apiview: APIViewModel) -> [String: ReviewLineData] { + 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 } diff --git a/src/swift/SwiftAPIViewCore/Tests/TestUtilityTests.swift b/src/swift/SwiftAPIViewCore/Tests/TestUtilityTests.swift index 0507abc374b..adab56db1c7 100644 --- a/src/swift/SwiftAPIViewCore/Tests/TestUtilityTests.swift +++ b/src/swift/SwiftAPIViewCore/Tests/TestUtilityTests.swift @@ -54,7 +54,7 @@ class TestUtilityTests: XCTestCase { options.hasSuffixSpace = false options.hasPrefixSpace = false line.tokens = [ReviewToken(kind: .text, value: "Some text", options: options)] - let model = APIViewModel(packageName: "Test", packageVersion: "0.0", statements: []) + 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" @@ -62,7 +62,7 @@ class TestUtilityTests: XCTestCase { } func testReviewLineTextWithChildren() throws { - let model = APIViewModel(packageName: "Test", packageVersion: "0.0", statements: []) + let model = CodeModel(packageName: "Test", packageVersion: "0.0", statements: []) var options = ReviewTokenOptions() options.hasSuffixSpace = false options.hasPrefixSpace = false @@ -84,7 +84,7 @@ class TestUtilityTests: XCTestCase { } func testSuffixSpaceBehavior() throws { - let model = APIViewModel(packageName: "Test", packageVersion: "0.0", statements: []) + let model = CodeModel(packageName: "Test", packageVersion: "0.0", statements: []) let line = ReviewLine() var options = ReviewTokenOptions() options.hasPrefixSpace = false @@ -100,7 +100,7 @@ class TestUtilityTests: XCTestCase { } func testPrefixSpaceBehavior() throws { - let model = APIViewModel(packageName: "Test", packageVersion: "0.0", statements: []) + let model = CodeModel(packageName: "Test", packageVersion: "0.0", statements: []) let line1 = ReviewLine() var options = ReviewTokenOptions() options.hasSuffixSpace = false From e410cac782646f22c9f7e9840edf58ea3625f71c Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Thu, 19 Dec 2024 12:04:57 -0600 Subject: [PATCH 21/29] Wire up navigation. --- .../Sources/Models/CodeModel.swift | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift index dd58382f238..a5946861612 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift @@ -81,9 +81,6 @@ class CodeModel: Tokenizable, Encodable { /// Used to track the currently processed namespace. private var namespaceStack = NamespaceStack() - /// Keeps track of which types have been declared. - private var typeDeclarations = Set() - /// Returns the text-based representation of all lines var text: String { var value = "" @@ -165,7 +162,6 @@ class CodeModel: Tokenizable, Encodable { } func newline() { - let text = self.currentLine.text() // ensure no trailing space at the end of the line if self.currentLine.tokens.count > 0 { let lastToken = currentLine.tokens.last @@ -185,7 +181,7 @@ class CodeModel: Tokenizable, Encodable { /// Set the exact number of desired newlines. func blankLines(set count: Int) { self.newline() - var parentLines = self.currentParent?.children ?? self.reviewLines + let parentLines = self.currentParent?.children ?? self.reviewLines // count the number of trailing newlines var newlineCount = 0 for line in parentLines.reversed() { @@ -249,17 +245,11 @@ class CodeModel: Tokenizable, Encodable { /// Register the declaration of a new type func typeDeclaration(name: String, typeId: String?, addCrossLanguageId: Bool = false, options: ReviewTokenOptions? = nil) { - if let typeId = typeId { - if self.typeDeclarations.contains(typeId) { - fatalError("Duplicate ID '\(typeId)' for declaration will result in bugs.") - } - self.typeDeclarations.insert(typeId) - } self.lineMarker(typeId, options: LineMarkerOptions(addCrossLanguageId: true)) var newOptions = options ?? ReviewTokenOptions() newOptions.navigationDisplayName = options?.navigationDisplayName ?? name newOptions.navigateToId = options?.navigateToId ?? typeId - self.token(kind: .typeName, value: name, options: options) + self.token(kind: .typeName, value: name, options: newOptions) } // // FIXME: This! @@ -278,7 +268,6 @@ class CodeModel: Tokenizable, Encodable { func typeReference(name: String, options: ReviewTokenOptions? = nil) { var newOptions = options ?? ReviewTokenOptions() newOptions.navigateToId = options?.navigateToId ?? CodeModel.missingId - newOptions.navigationDisplayName = options?.navigationDisplayName ?? name self.token(kind: .typeName, value: name, options: newOptions) } From 758063a1f40534a7bf02bdebff330f59b6b54af7 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Fri, 27 Dec 2024 10:36:48 -0600 Subject: [PATCH 22/29] Get line ID and review line metadata tests to pass for one test. --- .../Sources/Models/CodeModel.swift | 2 +- .../Sources/Models/DeclarationModel.swift | 1 + .../Tests/SwiftAPIViewCoreTests.swift | 18 ++++++++++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift index a5946861612..349d2216f57 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift @@ -216,7 +216,7 @@ class CodeModel: Tokenizable, Encodable { let token = ReviewToken(kind: .punctuation, value: value, options: options) if value == "}" { self.snap(token: token, to: "{") - self.currentLine.isContextEndLine = true + self.currentLine.isContextEndLine = self.currentLine.tokens.count == 1 } else if value == "(" { // Remove suffix space if preceding token is text if let lastToken = currentLine.tokens.last { diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift index 6b1143c386f..7f55481eb6a 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/DeclarationModel.swift @@ -161,6 +161,7 @@ class DeclarationModel: Tokenizable, Linkable, Equatable { for attr in children { let attrText = attr.withoutTrivia().description.filter { !$0.isWhitespace } a.lineMarker("\(definitionId!).\(attrText)") + a.currentLine.relatedToLine = definitionId attr.tokenize(apiview: a, parent: parent) a.blankLines(set: 0) } diff --git a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift index 6c20807a9bb..93c119e9f78 100644 --- a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift +++ b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift @@ -35,6 +35,10 @@ class SwiftAPIViewCoreTests: XCTestCase { var relatedToCount: Int; /// Counts the number of `isContextEndLine` var isContextEndCount: Int; + + var description: String { + return "relatedToCount: \(relatedToCount), isContextEndCount: \(isContextEndCount)" + } } override func setUpWithError() throws { @@ -103,7 +107,7 @@ class SwiftAPIViewCoreTests: XCTestCase { guard !lines.isEmpty else { return nil } var mainMap = [String: ReviewLineData]() var lastKey: String? = nil - for line in lines { + for (idx, line) in lines.enumerated() { let lineId = line.lineId if let related = line.relatedToLine { lastKey = related @@ -111,7 +115,7 @@ class SwiftAPIViewCoreTests: XCTestCase { subMap.relatedToCount += 1 mainMap[related] = subMap } - if let _ = line.isContextEndLine { + if line.isContextEndLine == true { guard lastKey != nil else { XCTFail("isEndContext found without a related line.") return nil @@ -132,6 +136,7 @@ class SwiftAPIViewCoreTests: XCTestCase { } } } + lastKey = lineId } return mainMap } @@ -149,7 +154,13 @@ class SwiftAPIViewCoreTests: XCTestCase { XCTFail("Key mismatch: \(lhsKeys.description) vs \(rhsKeys.description)") return } - XCTAssertEqual(lhs, rhs) + 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 @@ -168,7 +179,6 @@ class SwiftAPIViewCoreTests: XCTestCase { "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.MyRenamedProtocol": ReviewLineData(relatedToCount: 0, isContextEndCount: 0), "AttributesTestFile.swifttxt.MyStruct": ReviewLineData(relatedToCount: 2, isContextEndCount: 0), "AttributesTestFile.swifttxt.SomeSendable": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), ] From d41ff4f93b1f6ea369a3931347845b2530b98481 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Fri, 27 Dec 2024 11:13:17 -0600 Subject: [PATCH 23/29] Update all tests to check relatedTo and contextEnd. --- .../Tests/SwiftAPIViewCoreTests.swift | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift index 93c119e9f78..6d0c1c3ad0f 100644 --- a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift +++ b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift @@ -194,6 +194,16 @@ class SwiftAPIViewCoreTests: XCTestCase { 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 { @@ -205,6 +215,20 @@ class SwiftAPIViewCoreTests: XCTestCase { compare(expected: expected, actual: generated) validateLineIds(apiview: manager.model!) let counts = getRelatedLineMetadata(apiview: manager.model!) + // FIXME: Fix the wonky line IDs for extensions + 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), + "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 { @@ -216,6 +240,11 @@ class SwiftAPIViewCoreTests: XCTestCase { 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 { @@ -227,6 +256,24 @@ class SwiftAPIViewCoreTests: XCTestCase { compare(expected: expected, actual: generated) validateLineIds(apiview: manager.model!) let counts = getRelatedLineMetadata(apiview: manager.model!) + // FIXME: Fix extension line IDs + let expectedCounts: [String: ReviewLineData] = [ + "GenericsTestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), + "GenericsTestFile.swifttxt.Container": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), + "extensionContainer": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), + "extensionContainerwhereItem==Double": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), + "GenericsTestFile.swifttxt.ContainerAlt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), + "GenericsTestFile.swifttxt.ContainerStack": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), + "extensionContainerStack": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), + "GenericsTestFile.swifttxt.IntContainerStack": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), + "extensionIntContainerStack": 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), + "extensionStack": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), + "GenericsTestFile.swifttxt.SuffixableContainer": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), + ] + compareCounts(counts, expectedCounts) } func testInitializers() throws { @@ -238,6 +285,11 @@ class SwiftAPIViewCoreTests: XCTestCase { 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 { @@ -249,6 +301,15 @@ class SwiftAPIViewCoreTests: XCTestCase { compare(expected: expected, actual: generated) validateLineIds(apiview: manager.model!) let counts = getRelatedLineMetadata(apiview: manager.model!) + // FIXME: Fix extension line IDs + 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 { @@ -260,6 +321,10 @@ class SwiftAPIViewCoreTests: XCTestCase { compare(expected: expected, actual: generated) validateLineIds(apiview: manager.model!) let counts = getRelatedLineMetadata(apiview: manager.model!) + let expectedCounts: [String: ReviewLineData] = [ + "PrivateInternalTestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 0), + ] + compareCounts(counts, expectedCounts) } func testProperties() throws { @@ -271,6 +336,11 @@ class SwiftAPIViewCoreTests: XCTestCase { 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 { @@ -282,6 +352,37 @@ class SwiftAPIViewCoreTests: XCTestCase { 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": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), + "ProtocolTestFile.swifttxt.FullyNamed": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), + "ProtocolTestFile.swifttxt.Hamster": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), + "extensionHamster": 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.SomeClassOnlyProtocol": ReviewLineData(relatedToCount: 0, isContextEndCount: 0), + "ProtocolTestFile.swifttxt.SomeInitProtocol": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), + "ProtocolTestFile.swifttxt.SomeOtherClassOnlyProtocol": ReviewLineData(relatedToCount: 0, isContextEndCount: 0), + "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), + ] + compareCounts(counts, expectedCounts) } func testSwiftUI() throws { @@ -293,5 +394,11 @@ class SwiftAPIViewCoreTests: XCTestCase { 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) } } From 009cc92df4b0347e1649c26a3d3e86646221c469 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Mon, 30 Dec 2024 11:32:43 -0800 Subject: [PATCH 24/29] All tests passing! --- .../Sources/Models/ExtensionModel.swift | 52 ++++++++++--------- .../Sources/Models/NavigationToken.swift | 2 +- .../Sources/Models/PackageModel.swift | 4 +- .../Tests/SwiftAPIViewCoreTests.swift | 31 +++++------ 4 files changed, 45 insertions(+), 44 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ExtensionModel.swift index 3b276055a32..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,54 +73,53 @@ 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)) } } } @@ -131,7 +132,7 @@ class ExtensionModel: Tokenizable { } func tokenize(apiview a: CodeModel, parent: Linkable?) { - for (idx, child) in childNodes.enumerated() { + for child in childNodes { var options = ReviewTokenOptions() let childIdx = child.indexInParent if childIdx == 7 { @@ -181,7 +182,10 @@ extension Array { 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 { @@ -191,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/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 533d53eb19f..d313262efb2 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift @@ -142,13 +142,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() } diff --git a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift index 6d0c1c3ad0f..b9e8f11d07b 100644 --- a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift +++ b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift @@ -83,7 +83,9 @@ class SwiftAPIViewCoreTests: XCTestCase { func validate(line: ReviewLine) { // ensure there are no repeated definition IDs if let lineId = line.lineId { - XCTAssertFalse(lineIds.contains(lineId), "Duplicate line ID: \(lineId)") + if lineIds.contains(lineId) { + XCTFail("Duplicate line ID: \(lineId)") + } if lineId != "" { lineIds.insert(lineId) } @@ -215,7 +217,6 @@ class SwiftAPIViewCoreTests: XCTestCase { compare(expected: expected, actual: generated) validateLineIds(apiview: manager.model!) let counts = getRelatedLineMetadata(apiview: manager.model!) - // FIXME: Fix the wonky line IDs for extensions let expectedCounts: [String: ReviewLineData] = [ "ExtensionTestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), "ExtensionTestFile.swifttxt.Point": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), @@ -224,7 +225,7 @@ class SwiftAPIViewCoreTests: XCTestCase { "ExtensionTestFile.swifttxt.Size": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), "extensionDouble": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), "extensionInt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), - "Kind": 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), ] @@ -256,22 +257,22 @@ class SwiftAPIViewCoreTests: XCTestCase { compare(expected: expected, actual: generated) validateLineIds(apiview: manager.model!) let counts = getRelatedLineMetadata(apiview: manager.model!) - // FIXME: Fix extension line IDs let expectedCounts: [String: ReviewLineData] = [ "GenericsTestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), "GenericsTestFile.swifttxt.Container": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), - "extensionContainer": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), - "extensionContainerwhereItem==Double": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), "GenericsTestFile.swifttxt.ContainerAlt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), "GenericsTestFile.swifttxt.ContainerStack": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), - "extensionContainerStack": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), "GenericsTestFile.swifttxt.IntContainerStack": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), - "extensionIntContainerStack": 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), - "extensionStack": 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) } @@ -301,7 +302,6 @@ class SwiftAPIViewCoreTests: XCTestCase { compare(expected: expected, actual: generated) validateLineIds(apiview: manager.model!) let counts = getRelatedLineMetadata(apiview: manager.model!) - // FIXME: Fix extension line IDs let expectedCounts: [String: ReviewLineData] = [ "OperatorTestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), "OperatorTestFile.swifttxt.CongruentPrecedence": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), @@ -321,9 +321,7 @@ class SwiftAPIViewCoreTests: XCTestCase { compare(expected: expected, actual: generated) validateLineIds(apiview: manager.model!) let counts = getRelatedLineMetadata(apiview: manager.model!) - let expectedCounts: [String: ReviewLineData] = [ - "PrivateInternalTestFile.swifttxt": ReviewLineData(relatedToCount: 0, isContextEndCount: 0), - ] + let expectedCounts: [String: ReviewLineData] = [:] compareCounts(counts, expectedCounts) } @@ -358,10 +356,9 @@ class SwiftAPIViewCoreTests: XCTestCase { "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": 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), - "extensionHamster": 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), @@ -370,9 +367,7 @@ class SwiftAPIViewCoreTests: XCTestCase { "ProtocolTestFile.swifttxt.RandomNumberGenerator": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), "extensionRandomNumberGenerator": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), "ProtocolTestFile.swifttxt.SomeClass": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), - "ProtocolTestFile.swifttxt.SomeClassOnlyProtocol": ReviewLineData(relatedToCount: 0, isContextEndCount: 0), "ProtocolTestFile.swifttxt.SomeInitProtocol": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), - "ProtocolTestFile.swifttxt.SomeOtherClassOnlyProtocol": ReviewLineData(relatedToCount: 0, isContextEndCount: 0), "ProtocolTestFile.swifttxt.SomeOtherInitProtocol": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), "ProtocolTestFile.swifttxt.SomeProtocol": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), "ProtocolTestFile.swifttxt.SomeSubClass": ReviewLineData(relatedToCount: 0, isContextEndCount: 1), @@ -381,6 +376,8 @@ class SwiftAPIViewCoreTests: XCTestCase { "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) } From 84b05f4a2f3dba76c48d311b0e149466977a161a Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Mon, 30 Dec 2024 11:35:59 -0800 Subject: [PATCH 25/29] Clean up. --- .../Sources/Models/CodeModel.swift | 34 ------------------- .../Sources/Models/PackageModel.swift | 13 ------- .../Tests/SwiftAPIViewCoreTests.swift | 2 +- 3 files changed, 1 insertion(+), 48 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift index 349d2216f57..a9708c48f7d 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift @@ -252,18 +252,6 @@ class CodeModel: Tokenizable, Encodable { self.token(kind: .typeName, value: name, options: newOptions) } -// // FIXME: This! -// func findNonFunctionParent(from item: DeclarationModel?) -> 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, options: ReviewTokenOptions? = nil) { var newOptions = options ?? ReviewTokenOptions() @@ -271,27 +259,6 @@ class CodeModel: Tokenizable, Encodable { self.token(kind: .typeName, value: name, options: newOptions) } -// // FIXME: This! -// 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, options: ReviewTokenOptions? = nil) { self.token(kind: .memberName, value: name, options: options) } @@ -362,7 +329,6 @@ class CodeModel: Tokenizable, Encodable { self.currentLine = ReviewLine() } - // FIXME: This! /// 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 diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift index d313262efb2..f57779b2689 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/PackageModel.swift @@ -124,7 +124,6 @@ class PackageModel: Tokenizable, Linkable { a.blankLines(set: 0) options.applySpacing(SwiftSyntax.TokenKind.rightBrace.spacing) a.punctuation("}", options: options) - resolveTypeReferences(apiview: a) a.blankLines(set: 0) } @@ -170,18 +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: CodeModel) { - for line in a.reviewLines { - for (idx, token) in line.tokens.enumerated() { - // FIXME: Fix this up. -// guard token.navigateToId == CodeModel.unresolved else { continue } -// line.tokens[idx].navigateToId = a.definitionId(for: token.value!, withParent: nil) -// assert (line.tokens[idx].navigateToId != CodeModel.unresolved) - } - } - } - func appendIfVisible(_ decl: DeclarationModel) { if decl.shouldShow() { members.append(decl) diff --git a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift index b9e8f11d07b..b5c71dcc8fb 100644 --- a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift +++ b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift @@ -109,7 +109,7 @@ class SwiftAPIViewCoreTests: XCTestCase { guard !lines.isEmpty else { return nil } var mainMap = [String: ReviewLineData]() var lastKey: String? = nil - for (idx, line) in lines.enumerated() { + for line in lines { let lineId = line.lineId if let related = line.relatedToLine { lastKey = related From 114d42f17292d93dde8f6e82c9bd7fdf5e09a4ae Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Tue, 31 Dec 2024 10:12:27 -0800 Subject: [PATCH 26/29] Fix display anomalies. --- .../Sources/Models/CodeModel.swift | 24 ++++++++++--------- .../Tests/SwiftAPIViewCoreTests.swift | 14 +++++++++++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift b/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift index a9708c48f7d..dc595a63074 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/CodeModel.swift @@ -161,15 +161,20 @@ class CodeModel: Tokenizable, Encodable { self.token(kind: .text, value: text, options: options) } - func newline() { - // ensure no trailing space at the end of the line - if self.currentLine.tokens.count > 0 { - let lastToken = currentLine.tokens.last - lastToken?.hasSuffixSpace = false - let firstToken = currentLine.tokens.first - firstToken?.hasPrefixSpace = false + + /// 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 { @@ -299,10 +304,7 @@ class CodeModel: Tokenizable, Encodable { /// Wraps code in indentattion func indent(_ indentedCode: () -> Void) { - // ensure no trailing space at the end of the line - let lastToken = self.currentLine.tokens.last - lastToken?.hasSuffixSpace = false - + trimBeginningAndEnd() if let currentParent = self.currentParent { currentParent.children.append(self.currentLine) self.parentStack.append(currentParent) diff --git a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift index b5c71dcc8fb..26e1d9c5777 100644 --- a/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift +++ b/src/swift/SwiftAPIViewCore/Tests/SwiftAPIViewCoreTests.swift @@ -81,6 +81,20 @@ class SwiftAPIViewCoreTests: XCTestCase { 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) { From 82db43bde9348086b0c13a7ec628d239f556f406 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Wed, 8 Jan 2025 10:52:50 -0800 Subject: [PATCH 27/29] Code review feedback. --- .../Sources/Models/CodeDiagnostic.swift | 2 +- .../Sources/Models/ReviewLine.swift | 8 ++++++-- .../Sources/Models/ReviewToken.swift | 20 ++++++++++++++----- .../Sources/Models/ReviewTokenOptions.swift | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/CodeDiagnostic.swift b/src/swift/SwiftAPIViewCore/Sources/Models/CodeDiagnostic.swift index edd203a987c..88eccda6c30 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/CodeDiagnostic.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/CodeDiagnostic.swift @@ -32,7 +32,7 @@ enum CodeDiagnosticLevel: Int { 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.*/ + /// override fatal level system comments before approving a review. case Fatal = 4 } diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift index 01b7e38f634..703e2a4d9a5 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewLine.swift @@ -90,12 +90,16 @@ class ReviewLine: Tokenizable, Encodable { try container.encode(tokens, forKey: .tokens) try container.encodeIfPresent(lineId, forKey: .lineId) try container.encodeIfPresent(crossLanguageId, forKey: .crossLanguageId) - try container.encodeIfPresent(isHidden, forKey: .isHidden) - try container.encodeIfPresent(isContextEndLine, forKey: .isContextEndLine) 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 diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift index 2974789adfc..ffbf1497153 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewToken.swift @@ -106,11 +106,21 @@ class ReviewToken: Codable { try container.encode(kind.rawValue, forKey: .kind) try container.encodeIfPresent(navigationDisplayName, forKey: .navigationDisplayName) try container.encodeIfPresent(navigateToId, forKey: .navigateToId) - try container.encodeIfPresent(skipDiff, forKey: .skipDiff) - try container.encodeIfPresent(isDeprecated, forKey: .isDeprecated) - try container.encodeIfPresent(hasSuffixSpace, forKey: .hasSuffixSpace) - try container.encodeIfPresent(hasPrefixSpace, forKey: .hasPrefixSpace) - try container.encodeIfPresent(isDocumentation, forKey: .isDocumentation) + 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) } diff --git a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift index 912110eb131..3e2601e5d7c 100644 --- a/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift +++ b/src/swift/SwiftAPIViewCore/Sources/Models/ReviewTokenOptions.swift @@ -49,7 +49,7 @@ public struct ReviewTokenOptions { /// 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 */ + /// Set isDocumentation to true if current token is part of documentation var isDocumentation: Bool? /// Language specific style css class names From 9d3c4d3346c3227bf2e29e1403c053b294c7f708 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Wed, 8 Jan 2025 11:03:21 -0800 Subject: [PATCH 28/29] Bump version. Add CHANGELOG. --- src/swift/CHANGELOG.md | 3 +++ src/swift/SwiftAPIView/SwiftAPIView.xcodeproj/project.pbxproj | 4 ++-- .../SwiftAPIViewCore.xcodeproj/project.pbxproj | 4 ++-- .../Tests/ExpectFiles/AttributesExpectFile.txt | 2 +- .../Tests/ExpectFiles/EnumerationsExpectFile.txt | 2 +- .../Tests/ExpectFiles/ExtensionExpectFile.txt | 2 +- .../Tests/ExpectFiles/FunctionsExpectFile.txt | 2 +- .../SwiftAPIViewCore/Tests/ExpectFiles/GenericsExpectFile.txt | 2 +- .../Tests/ExpectFiles/InitializersExpectFile.txt | 2 +- .../SwiftAPIViewCore/Tests/ExpectFiles/OperatorExpectFile.txt | 2 +- .../Tests/ExpectFiles/PrivateInternalExpectFile.txt | 2 +- .../Tests/ExpectFiles/PropertiesExpectFile.txt | 2 +- .../SwiftAPIViewCore/Tests/ExpectFiles/ProtocolExpectFile.txt | 2 +- .../SwiftAPIViewCore/Tests/ExpectFiles/SwiftUIExpectFile.txt | 2 +- 14 files changed, 18 insertions(+), 15 deletions(-) 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/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/SwiftAPIViewCore.xcodeproj/project.pbxproj b/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj index e067d2e784a..c6cbcad07bd 100644 --- a/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj +++ b/src/swift/SwiftAPIViewCore/SwiftAPIViewCore.xcodeproj/project.pbxproj @@ -636,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; @@ -666,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/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 8d1b5d614f3..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 { 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 6b47b2fa5b2..f37345d2353 100644 --- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/PrivateInternalExpectFile.txt +++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/PrivateInternalExpectFile.txt @@ -1,4 +1,4 @@ -Package parsed using Swift APIView (version 0.2.2) +Package parsed using Swift APIView (version 0.3.0) 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 ebbfa218cdb..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 { diff --git a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/SwiftUIExpectFile.txt b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/SwiftUIExpectFile.txt index 93dd05d56ba..26b2bc5725c 100644 --- a/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/SwiftUIExpectFile.txt +++ b/src/swift/SwiftAPIViewCore/Tests/ExpectFiles/SwiftUIExpectFile.txt @@ -1,4 +1,4 @@ -Package parsed using Swift APIView (version 0.2.2) +Package parsed using Swift APIView (version 0.3.0) package SwiftUITestFile.swifttxt { From 7f9f2c63ce2b249d6b2cf32713bcbbc18e5e7676 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Wed, 8 Jan 2025 12:13:28 -0800 Subject: [PATCH 29/29] Code review feedback. --- .../Sources/SwiftAPIViewTests.h | 18 ------------------ .../project.pbxproj | 4 ---- 2 files changed, 22 deletions(-) delete mode 100644 src/swift/SwiftAPIViewTests/Sources/SwiftAPIViewTests.h diff --git a/src/swift/SwiftAPIViewTests/Sources/SwiftAPIViewTests.h b/src/swift/SwiftAPIViewTests/Sources/SwiftAPIViewTests.h deleted file mode 100644 index 9c8e00bf6f6..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; };