profile
viewpoint
Francisco Diaz fdiaz @airbnb San Francisco, California https://franciscodiaz.cl iOS Developer

airbnb/ResilientDecoding 498

This package makes your Decodable types resilient to decoding errors and allows you to inspect those errors.

airbnb/BuckSample 319

An example app showing how Buck can be used to build a simple iOS app.

dfed/CacheAdvance 22

A performant cache for logging systems. CacheAdvance persists log events 30x faster than SQLite.

dfed/XCTest-watchOS 18

XCTest-watchOS provides an implementation of XCTest that can run on watchOS

dfed/Floatation 3

Floatation is a lightweight dependency injection framework that makes it easy to surface dependencies to consuming code

fdiaz/9punto5-talk 0

De qué hablo cuando hablo de trabajo remoto

fdiaz/altconf-2018 0

I hate public speaking. So why do I keep doing it?

fdiaz/apollo-ios 0

📱 A strongly-typed, caching GraphQL client for iOS, written in Swift

fdiaz/atom 0

:atom: The hackable text editor

fdiaz/CombineWorkshop 0

Contains all files needed for my Combine workshop.

issue commentQuick/Nimble

Release Nimble v9 for bumping Swift version requirement (to 5.2) and adjust the deprecation roadmap

Now that Xcode 12 is GM it'd be nice to have a release version here if there's nothing else missing!

ikesyo

comment created time in 5 days

push eventfdiaz/SwiftInspector

Francisco Diaz

commit sha 8669c0d8c930c0e9b58e16fb4974bc27186b0b95

Update to use Xcode 12

view details

push time in 6 days

PR opened fdiaz/SwiftInspector

Update to use Swift 5.3

Closes #45

This PR is easier to review commit-by-commit

+24 -22

0 comment

8 changed files

pr created time in 6 days

push eventfdiaz/SwiftInspector

Francisco Diaz

commit sha 8aa3e4ccd1caa16bd311ac0fe1cbe4c46a3e1f1f

Update ArgumentParser call sites

view details

push time in 6 days

push eventfdiaz/SwiftInspector

Francisco Diaz

commit sha 3314c50bc9f7d1fa511f6c0f45a7991507c5e37e

Allow Quick to work with generate-xcodeproj See https://github.com/Quick/Quick/issues/707 for more information Fix Quick

view details

Francisco Diaz

commit sha 0f484ae82af7b2863622611d22f3991bf579b325

Disable failing test

view details

Francisco Diaz

commit sha 1b10356d646ea09593242ed1a6ec40a4c606ede6

Update Arguments Parser

view details

Francisco Diaz

commit sha b5e90115a8bb17c30047539d8b6125f7fc328327

Update ArgumentParser call sites

view details

push time in 6 days

push eventfdiaz/SwiftInspector

Francisco Diaz

commit sha aba12919629dc3e38cbe448e390908501a37e282

Allow Quick to work with generate-xcodeproj See https://github.com/Quick/Quick/issues/707 for more information

view details

Francisco Diaz

commit sha 758bcab213bbe2230ee128f1d1751805cb0a9159

Disable failing test

view details

Francisco Diaz

commit sha 53150a265854b3f4531a341120d99fa8f27d6020

Update Arguments Parser

view details

push time in 6 days

push eventfdiaz/SwiftInspector

Francisco Diaz

commit sha 43cd80e87ac94ff2c9c6067ad6e0fd6c6fd561df

Disable failing test

view details

Francisco Diaz

commit sha 495c19fcb5f74bc2ecdd6ab16746fc0aeda9323c

Update Arguments Parser

view details

push time in 6 days

push eventfdiaz/SwiftInspector

Francisco Diaz

commit sha 6592e247b40c5768b0d552da34172ddbb726fc77

Bump Swift version to 5.3.

view details

Francisco Diaz

commit sha a6ebed22f0d9a7eaec76e8ab8286dd93d71e10b6

Update Package to use SwiftSyntax 5.3

view details

Francisco Diaz

commit sha 9b54f2e9d41bf018a5fba35b363d5bf81a5d839f

Update Quick to 3.0

view details

push time in 6 days

issue openedfdiaz/SwiftInspector

Migrate to use Swift 5.3 by default

Now that Xcode 12 is in GM we should update this to work with Swift 5.3.

Tracking this work in https://github.com/fdiaz/SwiftInspector/tree/0.8.0b

created time in 6 days

create barnchfdiaz/SwiftInspector

branch : 0.8.0b

created branch time in 7 days

PullRequestReviewEvent

pull request commentairbnb/swift

Explicitly mention typealiases in the subsection organization rule

@bachand I like that! Maybe as a comment on top of https://github.com/airbnb/swift/blob/master/resources/airbnb.swiftformat for now?

calda

comment created time in 22 days

PullRequestReviewEvent
PullRequestReviewEvent

push eventfdiaz/SwiftInspector

Francisco Diaz

commit sha 99cd9bb4c5fc1fd3dde8b374475772cb9e7be8b9

[iOS] Rename executable from SwiftInspector to swiftinspector (#44) * Rename executable from SwiftInspector to swift-inspector * Remove hyphen

view details

push time in a month

delete branch fdiaz/SwiftInspector

delete branch : fd/improve-naming

delete time in a month

PR merged fdiaz/SwiftInspector

[iOS] Rename executable from SwiftInspector to swiftinspector

I started this project intending to call the executable SwiftInspector but pretty quickly it became clear that we should use swift-inspector for the binary name.

This PR accomplishes that by no longer renaming the binary after building for release.

+6 -7

1 comment

4 changed files

fdiaz

pr closed time in a month

push eventfdiaz/SwiftInspector

Francisco Diaz

commit sha 4e3650450139245649edd51cd8d550f6aae8c053

Remove hyphen

view details

push time in a month

Pull request review commentfdiaz/SwiftInspector

[iOS] Rename executable from SwiftInspector to swift-inspector

 let package = Package(     .macOS(.v10_13)   ],   products: [-    .executable(name: "SwiftInspector", targets: ["SwiftInspector"]),+    .executable(name: "swift-inspector", targets: ["SwiftInspector"]),

I like that, since we're changing the name lets go with the best option 👍

fdiaz

comment created time in a month

PullRequestReviewEvent

push eventfdiaz/SwiftInspector

Francisco Diaz

commit sha 93cff3806c0775a920b2289292571083c162d437

Rename executable from SwiftInspector to swift-inspector

view details

push time in a month

PR opened fdiaz/SwiftInspector

[iOS] Rename executable from SwiftInspector to swift-inspector

I started this project intending to call the executable SwiftInspector but pretty quickly it became clear that we should use swift-inspector for the binary name.

This PR accomplishes that by no longer renaming the binary after building for release.

+3 -4

0 comment

3 changed files

pr created time in a month

create barnchfdiaz/SwiftInspector

branch : fd/improve-naming

created branch time in a month

pull request commentfdiaz/SwiftInspector

add property analyzer

Thank you!

thedrick

comment created time in a month

push eventfdiaz/SwiftInspector

Tyler Hedrick

commit sha 7e9eceadf506f093b3593957b7373d5b340f0915

add property analyzer (#43) * add property analyzer * address comments * add new test for nested types * final comments addressed

view details

push time in a month

PR merged fdiaz/SwiftInspector

add property analyzer

Changes

This adds a new property analyzer and command. As with the TypeCommand / TypeAnalyzer this has more information in the analyzer than it does in the command.

Here's the command

swift-inspector properties --help
OVERVIEW: Finds property information for the provided type

USAGE: SwiftInspector properties --name <name> --path <path>

OPTIONS:
  --name <name>           The name of the type to find property information on 
        This may be a enum, class, struct, or protocol.
  --path <path>           The absolute path of the file to inspect 
  -h, --help              Show help information.

This command will find property information for a provided type including:

  • Type name
  • Each property includes:
    • Property name
    • Access (public, private, internal, fileprivate)
    • Scope (instance, scope)
    • Comment (Analyzer only)
    • Property type (Analyzer only)

Please review

@fdiaz

+1238 -0

4 comments

6 changed files

thedrick

pr closed time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - PropertyAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds property information for the provided type in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeProperties? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeProperties?+    let visitor = TypeSyntaxVisitor(typeName: typeName) { [unowned self] typeProperties in+      guard self.typeName == typeProperties.name else { return }+      result = try? typeProperties.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(typeName: String, onNodeVisit: @escaping (_ info: TypeProperties) -> Void) {+    self.onNodeVisit = onNodeVisit+    self.typeName = typeName+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    if node.identifier.text == typeName {+      processNode(node, withName: node.identifier.text, members: node.members.members)+    }+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    if node.identifier.text == typeName {+      processNode(node, withName: node.identifier.text, members: node.members.members)+    }+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    if node.identifier.text == typeName {+      processNode(node, withName: node.identifier.text, members: node.members.members)+    }+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    if node.identifier.text == typeName {+      processNode(node, withName: node.identifier.text, members: node.members.members)+    }+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if+      let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self),+      typeIdentifier.name.text == typeName+    {+      processNode(node, withName: typeIdentifier.name.text, members: node.members.members)+    }+    return .visitChildren+  }++  // MARK: Private++  private let onNodeVisit: (_ info: TypeProperties) -> Void+  private let typeName: String++  private func processNode<Node>(_ node: Node, withName name: String, members: MemberDeclListSyntax) where Node: SyntaxProtocol {+    var result = [TypeProperties.PropertyData]()+    let propertyVisitor = PropertySyntaxVisitor(typeName: name) { info in+      result.append(info)+    }+    propertyVisitor.walk(node)+    onNodeVisit(.init(name: name, properties: result))+  }+}++// MARK: - PropertySyntaxVisitor++private final class PropertySyntaxVisitor: SyntaxVisitor {++  init(typeName: String, onNodeVisit: @escaping (_ info: TypeProperties.PropertyData) -> Void) {+    self.typeName = typeName+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {+    var leadingTrivia: Trivia? = node.leadingTrivia+    let modifier = findModifiers(from: node)++    node.modifiers?.forEach { modifier in+      // leading trivia is on the first modifier, or the node itself if no modifiers are present+      leadingTrivia = leadingTrivia ?? modifier.leadingTrivia+    }++    node.bindings.forEach { binding in+      if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {+        if+          let typeAnnotation = binding.typeAnnotation,+          let simpleTypeIdentifier = typeAnnotation.type.as(SimpleTypeIdentifierSyntax.self)+        {+          onNodeVisit(.init(+            name: identifier.identifier.text,+            typeAnnotation: simpleTypeIdentifier.name.text,+            comment: comment(from: leadingTrivia),+            modifiers: modifier))+          return+        }+        // no type annotation was found. In this case it's much more complicated to get+        // the type information for a variable.+        // e.g.:+        // public let thing: String = "Hello" has a type annotation, String which makes this easy+        // public let thing = "Hello" does not have a type annotation, and I don't know how to handle this case.+        // TODO: Include logic to get types of any variable declaration+        onNodeVisit(.init(+          name: identifier.identifier.text,+          typeAnnotation: nil,+          comment: comment(from: leadingTrivia),+          modifiers: modifier))+      }+    }++    return .skipChildren+  }++  // MARK: Private++  private let typeName: String+  private let onNodeVisit: (_ info: TypeProperties.PropertyData) -> Void++  private func comment(from trivia: Trivia?) -> String {+    guard let trivia = trivia else { return "" }+    return trivia.compactMap { piece -> String? in+      switch piece {+      case .lineComment(let str): return str+      case .blockComment(let str): return str+      case .docLineComment(let str): return str+      case .docBlockComment(let str): return str+      default: return nil+      }+    }.joined(separator: "\n")+  }++  private func findModifiers(from node: VariableDeclSyntax) -> TypeProperties.Modifier {+    let modifiersString: [String] = node.children+      .compactMap { $0.as(ModifierListSyntax.self) }+      .reduce(into: []) { result, syntax in+        let modifiers: [String] = syntax.children+          .compactMap { $0.as(DeclModifierSyntax.self) }+          .map { modifierSyntax in+            if+              let leftParen = modifierSyntax.detailLeftParen,+              let detail = modifierSyntax.detail,+              let rightParen = modifierSyntax.detailRightParen+            {+              return modifierSyntax.name.text + leftParen.text + detail.text + rightParen.text+            }+            return modifierSyntax.name.text+        }+        result.append(contentsOf: modifiers)+      }++    var modifier = modifiersString.reduce(TypeProperties.Modifier()) { result, stringValue in+      let modifier = TypeProperties.Modifier(stringValue: stringValue)+      return result.union(modifier)+    }++    // If there are no explicit modifiers, this is an internal property+    if !modifier.contains(.public) &&+      !modifier.contains(.fileprivate) &&+      !modifier.contains(.private)+    {+      modifier = modifier.union(.internal)+    }++    // If the variable isn't static, it's an instance variable+    if !modifier.contains(.static) {+      modifier = modifier.union(.instance)+    }++    return modifier+  }+}++// MARK: - TypeProperties++/// Information about a type as well as its associated properties+public struct TypeProperties: Hashable {++  public struct Modifier: Hashable, OptionSet {+    public let rawValue: Int++    public static let `internal` = Modifier(rawValue: 1 << 0)+    public static let `public` = Modifier(rawValue: 1 << 1)+    public static let `private` = Modifier(rawValue: 1 << 2)+    public static let privateSet = Modifier(rawValue: 1 << 3)+    public static let `fileprivate` = Modifier(rawValue: 1 << 4)+    public static let `instance` = Modifier(rawValue: 1 << 5)+    public static let `static` = Modifier(rawValue: 1 << 6)++    public init(rawValue: Int)  {+      self.rawValue = rawValue+    }++    public init(stringValue: String) {+      switch stringValue {+      case "public": self = .public+      case "private": self = .private+      case "private(set)": self = .privateSet+      case "fileprivate": self = .fileprivate+      case "internal": self = .internal+      case "static": self = .static+      default: self = []+      }+    }+  }++  public struct PropertyData: Hashable {+    /// The name of the property+    public let name: String+    /// The Type annotation of the property if it's present+    public let typeAnnotation: String?+    /// Any comments associated with the property+    public let comment: String+    /// Modifier set for this type+    public let modifiers: Modifier+  }++  /// The name of the type.+  public let name: String+  /// The properties on this type+  public let properties: [PropertyData]+}++extension TypeProperties {+  struct MergeError: Error, CustomStringConvertible {++    init(errorMessage: String) {+      self.errorMessage = errorMessage+    }++    var description: String { errorMessage }++    private let errorMessage: String+  }+}++extension TypeProperties.MergeError {+  static var invalidNames = TypeProperties.MergeError(+    errorMessage: "Invalid types - the type names must match to be merged.")+}++extension TypeProperties {+  func merge(with other: TypeProperties?) throws -> TypeProperties {+    guard let other = other else {+      return self+    }+    guard name == other.name else {+      throw MergeError.invalidNames+    }+    return TypeProperties(+      name: name,+      properties: Array(Set(properties).union(Set(other.properties))))+  }+}++extension TypeProperties.Modifier: CustomStringConvertible, CustomDebugStringConvertible {

Let's move this extension to the Command since this is related to how we output things to the standard output which the Analyzer shouldn't care about see: https://github.com/fdiaz/SwiftInspector/blob/main/CONTRIBUTING.md#swiftinspectorcommand

thedrick

comment created time in a month

PullRequestReviewEvent

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - PropertyAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds property information for the provided type in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeProperties? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeProperties?+    let visitor = TypeSyntaxVisitor(typeName: typeName) { [unowned self] typeProperties in+      guard self.typeName == typeProperties.name else { return }+      result = try? typeProperties.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(typeName: String, onNodeVisit: @escaping (_ info: TypeProperties) -> Void) {+    self.onNodeVisit = onNodeVisit+    self.typeName = typeName+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    if node.identifier.text == typeName {+      processNode(node, withName: node.identifier.text, members: node.members.members)+    }+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    if node.identifier.text == typeName {+      processNode(node, withName: node.identifier.text, members: node.members.members)+    }+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    if node.identifier.text == typeName {+      processNode(node, withName: node.identifier.text, members: node.members.members)+    }+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    if node.identifier.text == typeName {+      processNode(node, withName: node.identifier.text, members: node.members.members)+    }+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if+      let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self),+      typeIdentifier.name.text == typeName+    {+      processNode(node, withName: typeIdentifier.name.text, members: node.members.members)+    }+    return .visitChildren+  }++  // MARK: Private++  private let onNodeVisit: (_ info: TypeProperties) -> Void+  private let typeName: String++  private func processNode<Node>(_ node: Node, withName name: String, members: MemberDeclListSyntax) where Node: SyntaxProtocol {+    var result = [TypeProperties.PropertyData]()+    let propertyVisitor = PropertySyntaxVisitor(typeName: name) { info in+      result.append(info)+    }+    propertyVisitor.walk(node)+    onNodeVisit(.init(name: name, properties: result))+  }+}++// MARK: - PropertySyntaxVisitor++private final class PropertySyntaxVisitor: SyntaxVisitor {++  init(typeName: String, onNodeVisit: @escaping (_ info: TypeProperties.PropertyData) -> Void) {+    self.typeName = typeName+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {+    var leadingTrivia: Trivia? = node.leadingTrivia+    let modifier = findModifiers(from: node)++    node.modifiers?.forEach { modifier in+      // leading trivia is on the first modifier, or the node itself if no modifiers are present+      leadingTrivia = leadingTrivia ?? modifier.leadingTrivia+    }++    node.bindings.forEach { binding in+      if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {+        if+          let typeAnnotation = binding.typeAnnotation,+          let simpleTypeIdentifier = typeAnnotation.type.as(SimpleTypeIdentifierSyntax.self)+        {+          onNodeVisit(.init(+            name: identifier.identifier.text,+            typeAnnotation: simpleTypeIdentifier.name.text,+            comment: comment(from: leadingTrivia),+            modifiers: modifier))+          return+        }+        // no type annotation was found. In this case it's much more complicated to get+        // the type information for a variable.+        // e.g.:+        // public let thing: String = "Hello" has a type annotation, String which makes this easy+        // public let thing = "Hello" does not have a type annotation, and I don't know how to handle this case.+        // TODO: Include logic to get types of any variable declaration+        onNodeVisit(.init(+          name: identifier.identifier.text,+          typeAnnotation: nil,+          comment: comment(from: leadingTrivia),+          modifiers: modifier))+      }+    }++    return .skipChildren+  }++  // MARK: Private++  private let typeName: String+  private let onNodeVisit: (_ info: TypeProperties.PropertyData) -> Void++  private func comment(from trivia: Trivia?) -> String {+    guard let trivia = trivia else { return "" }+    return trivia.compactMap { piece -> String? in+      switch piece {+      case .lineComment(let str): return str+      case .blockComment(let str): return str+      case .docLineComment(let str): return str+      case .docBlockComment(let str): return str+      default: return nil+      }+    }.joined(separator: "\n")+  }++  private func findModifiers(from node: VariableDeclSyntax) -> TypeProperties.Modifier {+    let modifiersString: [String] = node.children+      .compactMap { $0.as(ModifierListSyntax.self) }+      .reduce(into: []) { result, syntax in+        let modifiers: [String] = syntax.children+          .compactMap { $0.as(DeclModifierSyntax.self) }+          .map { modifierSyntax in+            if+              let leftParen = modifierSyntax.detailLeftParen,+              let detail = modifierSyntax.detail,+              let rightParen = modifierSyntax.detailRightParen+            {+              return modifierSyntax.name.text + leftParen.text + detail.text + rightParen.text+            }+            return modifierSyntax.name.text+        }+        result.append(contentsOf: modifiers)+      }++    var modifier = modifiersString.reduce(TypeProperties.Modifier()) { result, stringValue in+      let modifier = TypeProperties.Modifier(stringValue: stringValue)+      return result.union(modifier)+    }++    // If there are no explicit modifiers, this is an internal property+    if !modifier.contains(.public) &&+      !modifier.contains(.fileprivate) &&+      !modifier.contains(.private)+    {+      modifier = modifier.union(.internal)+    }++    // If the variable isn't static, it's an instance variable+    if !modifier.contains(.static) {+      modifier = modifier.union(.instance)+    }++    return modifier+  }+}++// MARK: - TypeProperties++/// Information about a type as well as its associated properties+public struct TypeProperties: Hashable {++  public struct Modifier: Hashable, OptionSet {+    public let rawValue: Int++    public static let `internal` = Modifier(rawValue: 1 << 0)+    public static let `public` = Modifier(rawValue: 1 << 1)+    public static let `private` = Modifier(rawValue: 1 << 2)+    public static let privateSet = Modifier(rawValue: 1 << 3)

Should we include internal(set) and public(set)?

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/18/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick+import Foundation++@testable import SwiftInspectorCore++final class PropertyAnalyzerSpec: QuickSpec {+  override func spec() {+    var fileURL: URL!+    var sut = PropertyAnalyzer(typeName: "FakeType")++    beforeEach {+      sut = PropertyAnalyzer(typeName: "FakeType")+    }++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }++    describe("analyze(fileURL:)") {+      context("when there are no properties") {+        beforeEach {+          let content = """+                        public final class FakeType {}+                        """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns type info with empty property list") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties).to(beEmpty())+        }+      }++      context("when there is a property") {+        beforeEach {+          let content = """+          public final class FakeType {+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("returns nil if the type name is not present") {+          let sut = PropertyAnalyzer(typeName: "AnotherType")+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).to(beNil())+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeProperties.PropertyData(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              modifiers: [.public, .instance])+          ]+        }+      }++      context("when there is a property in a nested type with the same type name") {+        beforeEach {+          let content = """+          public final class FakeType {+            public var thing: String = "Hello, World"++            enum FakeType {+              static let foo: String = "Hola"+            }+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        /*+         This is actually not the ideal result of this and is a limitation of the implementation+         Ideally you would have to pass in `FakeType.FakeType` to get this nested type's property+         information. For now we are accepting this limitation and have this test to showcase+         what happens in this scenario.+         */+        it("detects and merges the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          let propSet = Set(result?.properties ?? [])+          let expectedPropSet: Set<TypeProperties.PropertyData> = [+            .init(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              modifiers: [.public, .instance]),+            .init(+              name: "foo",+              typeAnnotation: "String",+              comment: "",+              modifiers: [.internal, .static])+          ]+          expect(propSet) == expectedPropSet+        }

Agreed with Michael, it's great to have a test like this and document that it's not ideal but it's what a workaround we're ok with.

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - PropertyAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds property information for the provided type in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeProperties? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeProperties?+    let visitor = TypeSyntaxVisitor(typeName: typeName) { [unowned self] typeProperties in+      guard self.typeName == typeProperties.name else { return }+      result = try? typeProperties.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(typeName: String, onNodeVisit: @escaping (_ info: TypeProperties) -> Void) {+    self.onNodeVisit = onNodeVisit+    self.typeName = typeName+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    if node.identifier.text == typeName {+      processNode(node, withName: node.identifier.text, members: node.members.members)+    }+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    if node.identifier.text == typeName {+      processNode(node, withName: node.identifier.text, members: node.members.members)+    }+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    if node.identifier.text == typeName {+      processNode(node, withName: node.identifier.text, members: node.members.members)+    }+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    if node.identifier.text == typeName {+      processNode(node, withName: node.identifier.text, members: node.members.members)+    }+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if+      let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self),+      typeIdentifier.name.text == typeName+    {+      processNode(node, withName: typeIdentifier.name.text, members: node.members.members)+    }+    return .visitChildren+  }++  // MARK: Private++  private let onNodeVisit: (_ info: TypeProperties) -> Void+  private let typeName: String++  private func processNode<Node>(_ node: Node, withName name: String, members: MemberDeclListSyntax) where Node: SyntaxProtocol {+    var result = [TypeProperties.PropertyData]()+    let propertyVisitor = PropertySyntaxVisitor(typeName: name) { info in+      result.append(info)+    }+    propertyVisitor.walk(node)+    onNodeVisit(.init(name: name, properties: result))+  }+}++// MARK: - PropertySyntaxVisitor++private final class PropertySyntaxVisitor: SyntaxVisitor {++  init(typeName: String, onNodeVisit: @escaping (_ info: TypeProperties.PropertyData) -> Void) {+    self.typeName = typeName+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {+    var leadingTrivia: Trivia? = node.leadingTrivia+    let modifier = findModifiers(from: node)++    node.modifiers?.forEach { modifier in+      // leading trivia is on the first modifier, or the node itself if no modifiers are present+      leadingTrivia = leadingTrivia ?? modifier.leadingTrivia+    }++    node.bindings.forEach { binding in+      if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {+        if+          let typeAnnotation = binding.typeAnnotation,+          let simpleTypeIdentifier = typeAnnotation.type.as(SimpleTypeIdentifierSyntax.self)+        {+          onNodeVisit(.init(+            name: identifier.identifier.text,+            typeAnnotation: simpleTypeIdentifier.name.text,+            comment: comment(from: leadingTrivia),+            modifiers: modifier))+          return+        }+        // no type annotation was found. In this case it's much more complicated to get+        // the type information for a variable.+        // e.g.:+        // public let thing: String = "Hello" has a type annotation, String which makes this easy+        // public let thing = "Hello" does not have a type annotation, and I don't know how to handle this case.+        // TODO: Include logic to get types of any variable declaration+        onNodeVisit(.init(+          name: identifier.identifier.text,+          typeAnnotation: nil,+          comment: comment(from: leadingTrivia),+          modifiers: modifier))+      }+    }++    return .skipChildren+  }++  // MARK: Private++  private let typeName: String+  private let onNodeVisit: (_ info: TypeProperties.PropertyData) -> Void++  private func comment(from trivia: Trivia?) -> String {+    guard let trivia = trivia else { return "" }+    return trivia.compactMap { piece -> String? in+      switch piece {+      case .lineComment(let str): return str+      case .blockComment(let str): return str+      case .docLineComment(let str): return str+      case .docBlockComment(let str): return str+      default: return nil+      }+    }.joined(separator: "\n")+  }++  private func findModifiers(from node: VariableDeclSyntax) -> TypeProperties.Modifier {+    let modifiersString: [String] = node.children+      .compactMap { $0.as(ModifierListSyntax.self) }+      .reduce(into: []) { result, syntax in+        let modifiers: [String] = syntax.children+          .compactMap { $0.as(DeclModifierSyntax.self) }+          .map { modifierSyntax in+            if+              let leftParen = modifierSyntax.detailLeftParen,+              let detail = modifierSyntax.detail,+              let rightParen = modifierSyntax.detailRightParen+            {+              return modifierSyntax.name.text + leftParen.text + detail.text + rightParen.text+            }+            return modifierSyntax.name.text+        }+        result.append(contentsOf: modifiers)+      }++    var modifier = modifiersString.reduce(TypeProperties.Modifier()) { result, stringValue in+      let modifier = TypeProperties.Modifier(stringValue: stringValue)+      return result.union(modifier)+    }++    // If there are no explicit modifiers, this is an internal property+    if !modifier.contains(.public) &&+      !modifier.contains(.fileprivate) &&+      !modifier.contains(.private)+    {+      modifier = modifier.union(.internal)+    }++    // If the variable isn't static, it's an instance variable+    if !modifier.contains(.static) {+      modifier = modifier.union(.instance)+    }++    return modifier+  }+}++// MARK: - TypeProperties++/// Information about a type as well as its associated properties+public struct TypeProperties: Hashable {++  public struct Modifier: Hashable, OptionSet {+    public let rawValue: Int++    public static let `internal` = Modifier(rawValue: 1 << 0)+    public static let `public` = Modifier(rawValue: 1 << 1)+    public static let `private` = Modifier(rawValue: 1 << 2)+    public static let privateSet = Modifier(rawValue: 1 << 3)+    public static let `fileprivate` = Modifier(rawValue: 1 << 4)+    public static let `instance` = Modifier(rawValue: 1 << 5)+    public static let `static` = Modifier(rawValue: 1 << 6)++    public init(rawValue: Int)  {+      self.rawValue = rawValue+    }++    public init(stringValue: String) {+      switch stringValue {+      case "public": self = .public+      case "private": self = .private+      case "private(set)": self = .privateSet+      case "fileprivate": self = .fileprivate+      case "internal": self = .internal+      case "static": self = .static+      default: self = []+      }+    }+  }++  public struct PropertyData: Hashable {+    /// The name of the property+    public let name: String+    /// The Type annotation of the property if it's present+    public let typeAnnotation: String?+    /// Any comments associated with the property+    public let comment: String+    /// Modifier set for this type+    public let modifiers: Modifier+  }++  /// The name of the type.+  public let name: String+  /// The properties on this type+  public let properties: [PropertyData]+}++extension TypeProperties {+  struct MergeError: Error, CustomStringConvertible {++    init(errorMessage: String) {+      self.errorMessage = errorMessage+    }++    var description: String { errorMessage }++    private let errorMessage: String+  }+}++extension TypeProperties.MergeError {+  static var invalidNames = TypeProperties.MergeError(+    errorMessage: "Invalid types - the type names must match to be merged.")+}++extension TypeProperties {+  func merge(with other: TypeProperties?) throws -> TypeProperties {+    guard let other = other else {+      return self+    }+    guard name == other.name else {+      throw MergeError.invalidNames+    }+    return TypeProperties(+      name: name,+      properties: Array(Set(properties).union(Set(other.properties))))+  }+}++extension TypeProperties.Modifier: CustomStringConvertible, CustomDebugStringConvertible {+  public var description: String {+    var outputValues: [String] = []+    // Order is important here! Access control modifiers should always go before scope modifiers+    // Access control modifiers+    if contains(.public) { outputValues.append("public") }+    if contains(.private) { outputValues.append("private") }+    if contains(.privateSet) { outputValues.append("private(set)") }

Should setters go after fileprivate and internal?

thedrick

comment created time in a month

PullRequestReviewEvent

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 3/27/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick+import Foundation++@testable import SwiftInspectorCore++final class PropertyAnalyzerSpec: QuickSpec {+  override func spec() {+    var fileURL: URL!+    var sut = PropertyAnalyzer(typeName: "FakeType")++    beforeEach {+      sut = PropertyAnalyzer(typeName: "FakeType")+    }++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }++    describe("TypeWithPropInfo.merge(other:)") {+      let type1 = TypeWithPropInfo(+        name: "MyType",+        properties: [+          TypeWithPropInfo.PropInfo(name: "thing", typeAnnotation: "String", comment: "", access: .public, scope: .instance)+      ])+      let type2 = TypeWithPropInfo(+        name: "MyType",+        properties: [+          TypeWithPropInfo.PropInfo(name: "foo", typeAnnotation: "Int", comment: "", access: .public, scope: .instance)+      ])+      let type3 = TypeWithPropInfo(+        name: "AnotherType",+        properties: [+          TypeWithPropInfo.PropInfo(name: "foo", typeAnnotation: "Int", comment: "", access: .public, scope: .instance)+      ])++      context("when both types have the same name") {+        let result = try? type1.merge(with: type2)+        it("succeeds") {+          expect(result?.name) == "MyType"+        }++        it("has merged props") {+          let set = Set(result?.properties ?? [])+          let expectedSet: Set<TypeWithPropInfo.PropInfo> = [+            TypeWithPropInfo.PropInfo(name: "thing", typeAnnotation: "String", comment: "", access: .public, scope: .instance),+            TypeWithPropInfo.PropInfo(name: "foo", typeAnnotation: "Int", comment: "", access: .public, scope: .instance)+          ]+          expect(set) == expectedSet+        }+      }++      context("when the types don't match") {+        it("fails to merge and asserts") {+          expect { try type1.merge(with: type3) }.to(throwError())+        }+      }++      context("when the other type is nil") {+        it("returns the original") {+          expect(try? type1.merge(with: nil)) == type1+        }+      }+    }++    describe("analyze(fileURL:)") {+      context("when there are no properties") {+        beforeEach {+          let content = """+                        public final class FakeType {}+                        """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns type info with empty property list") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties).to(beEmpty())+        }+      }++      context("when there is a property") {+        beforeEach {+          let content = """+          public final class FakeType {+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("returns nil if the type name is not present") {+          let sut = PropertyAnalyzer(typeName: "AnotherType")+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).to(beNil())+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .public,+              scope: .instance)+          ]+        }+      }++      context("when there is a property (struct)") {+        beforeEach {+          let content = """+          public struct FakeType {+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .public,+              scope: .instance)+          ]+        }+      }++      context("when there is a property (enum)") {+        beforeEach {+          let content = """+          public enum FakeType {+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .public,+              scope: .instance)+          ]+        }+      }++      context("when there is a property (protocol)") {+        beforeEach {+          let content = """+          public protocol FakeType {+            var thing: String { get }+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .internal,+              scope: .instance)+          ]+        }+      }++      context("when there is a property extension") {+        beforeEach {+          let content = """+          public class FakeType { }++          extension FakeType {+            var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .internal,+              scope: .instance)+          ]+        }+      }++      context("when there are multiple properites") {+        beforeEach {+          let content = """+          public final class FakeType {+            public var thing: String = "Hello, World"+            var foo: Int = 4+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          let propSet = Set(result?.properties ?? [])+          let expectedPropSet: Set<TypeWithPropInfo.PropInfo> = [+            .init(name: "thing", typeAnnotation: "String", comment: "", access: .public, scope: .instance),+            .init(name: "foo", typeAnnotation: "Int", comment: "", access: .internal, scope: .instance)+          ]+          expect(propSet) == expectedPropSet+        }+      }++      context("when there is a static property") {+        beforeEach {+          let content = """+          public final class FakeType {+            public static var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .public,+              scope: .static)+          ]+        }+      }++      context("when there is a private property") {+        beforeEach {+          let content = """+          public final class FakeType {+            private static var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .private,+              scope: .static)+          ]+        }+      }++      context("when there is a fileprivate property") {+        beforeEach {+          let content = """+          public final class FakeType {+            fileprivate static var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .fileprivate,+              scope: .static)+          ]+        }+      }++      context("when there is no type annotation") {+        beforeEach {+          let content = """+          public final class FakeType {+            private static var thing = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property with no type information") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: nil,+              comment: "",+              access: .private,+              scope: .static)+          ]+        }+      }++      context("when there is a line comment") {+        beforeEach {+          let content = """+          public final class FakeType {+            // The thing+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property with the comment") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "// The thing",+              access: .public,+              scope: .instance)+          ]+        }+      }++      context("when there is a doc line comment") {+        beforeEach {+          let content = """+          public final class FakeType {+            /// The thing+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property with the comment") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "/// The thing",+              access: .public,+              scope: .instance)+          ]+        }+      }++      context("when there is a block comment") {+        beforeEach {+          let content = """+          public final class FakeType {+            /* The thing */+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property with the comment") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "/* The thing */",+              access: .public,+              scope: .instance)+          ]+        }+      }++      context("when there is a doc block comment") {+        beforeEach {+          let content = """+          public final class FakeType {+            /** The thing */+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property with the comment") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "/** The thing */",+              access: .public,+              scope: .instance)+          ]+        }+      }++    }+  }+}

I agree, let's just add it as a follow-up if needed.

thedrick

comment created time in a month

PullRequestReviewEvent

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 3/27/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick+import Foundation++@testable import SwiftInspectorCore++final class PropertyAnalyzerSpec: QuickSpec {+  override func spec() {+    var fileURL: URL!+    var sut = PropertyAnalyzer(typeName: "FakeType")++    beforeEach {+      sut = PropertyAnalyzer(typeName: "FakeType")+    }++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }++    describe("TypeWithPropInfo.merge(other:)") {+      let type1 = TypeWithPropInfo(+        name: "MyType",+        properties: [+          TypeWithPropInfo.PropInfo(name: "thing", typeAnnotation: "String", comment: "", access: .public, scope: .instance)+      ])+      let type2 = TypeWithPropInfo(+        name: "MyType",+        properties: [+          TypeWithPropInfo.PropInfo(name: "foo", typeAnnotation: "Int", comment: "", access: .public, scope: .instance)+      ])+      let type3 = TypeWithPropInfo(+        name: "AnotherType",+        properties: [+          TypeWithPropInfo.PropInfo(name: "foo", typeAnnotation: "Int", comment: "", access: .public, scope: .instance)+      ])++      context("when both types have the same name") {+        let result = try? type1.merge(with: type2)+        it("succeeds") {+          expect(result?.name) == "MyType"+        }++        it("has merged props") {+          let set = Set(result?.properties ?? [])+          let expectedSet: Set<TypeWithPropInfo.PropInfo> = [+            TypeWithPropInfo.PropInfo(name: "thing", typeAnnotation: "String", comment: "", access: .public, scope: .instance),+            TypeWithPropInfo.PropInfo(name: "foo", typeAnnotation: "Int", comment: "", access: .public, scope: .instance)+          ]+          expect(set) == expectedSet+        }+      }++      context("when the types don't match") {+        it("fails to merge and asserts") {+          expect { try type1.merge(with: type3) }.to(throwError())+        }+      }++      context("when the other type is nil") {+        it("returns the original") {+          expect(try? type1.merge(with: nil)) == type1+        }+      }+    }++    describe("analyze(fileURL:)") {+      context("when there are no properties") {+        beforeEach {+          let content = """+                        public final class FakeType {}+                        """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns type info with empty property list") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties).to(beEmpty())+        }+      }++      context("when there is a property") {+        beforeEach {+          let content = """+          public final class FakeType {

I think this is fine for now. Let's keep it as is and if we see problems in the real world or a use case where we need to handle this we can build on top of this.

thedrick

comment created time in a month

PullRequestReviewEvent

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/17/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 ArgumentParser+import Foundation+import SwiftInspectorCore++final class PropertyAnalyzerCommand: ParsableCommand {+  static var configuration = CommandConfiguration(+    commandName: "properties",+    abstract: "Finds property information for the provided type"+  )++  @Option(help: nameArgumentHelp)+  var name: String++  @Option(help: "The absolute path of the file to inspect")+  var path: String++  /// Runs the command+  func run() throws {+    let cachedSyntaxTree = CachedSyntaxTree()++    let analyzer = PropertyAnalyzer(typeName: name, cachedSyntaxTree: cachedSyntaxTree)+    let fileURL = URL(fileURLWithPath: path)+    if let propertyInformation = try analyzer.analyze(fileURL: fileURL) {+      print(outputString(from: propertyInformation))+    }+  }++  /// Validates if the arguments of this command are valid+  func validate() throws {+    guard !name.isEmpty else {+      throw InspectorError.emptyArgument(argumentName: "--name")+    }+    guard !path.isEmpty else {+      throw InspectorError.emptyArgument(argumentName: "--path")+    }+    let pathURL = URL(fileURLWithPath: path)+    guard FileManager.default.isSwiftFile(at: pathURL) else {+      throw InspectorError.invalidArgument(argumentName: "--path", value: path)+    }+  }++  private func outputString(from statement: TypeWithPropInfo) -> String {+    let propString = statement.properties.map { propInfo in+      "(\(propInfo.name)|\(propInfo.access)|\(propInfo.scope))"

I think returning the type is useful when parsing the result. That way you don't need to inspect both what you're passing and what the output is giving you. I've found it useful at least

thedrick

comment created time in a month

PullRequestReviewEvent

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeWithPropInfo? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeWithPropInfo?+    let visitor = TypeSyntaxVisitor() { [unowned self] typeWithPropInfo in+      guard self.typeName == typeWithPropInfo.name else { return }+      result = try? typeWithPropInfo.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeLocationSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(onNodeVisit: @escaping (_ info: TypeWithPropInfo) -> Void) {+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self) {+      processNode(node, withName: typeIdentifier.name.text, members: node.members.members)+    }+    return .visitChildren+  }++  // MARK: Private++  private let onNodeVisit: (_ info: TypeWithPropInfo) -> Void++  private func processNode<Node>(_ node: Node, withName name: String, members: MemberDeclListSyntax) where Node: SyntaxProtocol {+    var result = [TypeWithPropInfo.PropInfo]()+    let propertyVisitor = PropertySyntaxVisitor(parentName: name) { info in+      result.append(info)+    }+    propertyVisitor.walk(node)+    onNodeVisit(.init(name: name, properties: result))+  }+}++// MARK: - PropertySyntaxVisitor++private final class PropertySyntaxVisitor: SyntaxVisitor {++  init(parentName: String, onNodeVisit: @escaping (_ info: TypeWithPropInfo.PropInfo) -> Void) {+    self.parentName = parentName+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {+    var leadingTrivia: Trivia? = node.leadingTrivia+    var access: TypeWithPropInfo.Access = .internal+    var scope: TypeWithPropInfo.Scope = .instance++    node.modifiers?.forEach { modifier in+      // leading trivia is on the first modifier, or the node itself if no modifiers are present+      leadingTrivia = leadingTrivia ?? modifier.leadingTrivia+      // This attempts to get any access information from the node's modifiers+      if let modifierAccess = TypeWithPropInfo.Access(rawValue: modifier.name.text) {+        access = modifierAccess+      }+      // this attempts to get any scope information from the node's modifiers+      if let modifierScope = TypeWithPropInfo.Scope(rawValue: modifier.name.text) {+        scope = modifierScope+      }+    }++    // in practice this `bindings` array only has 1 element+    node.bindings.forEach { binding in+      if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {+        if+          let typeAnnotation = binding.typeAnnotation,+          let simpleTypeIdentifier = typeAnnotation.type.as(SimpleTypeIdentifierSyntax.self)+        {+          onNodeVisit(.init(+            name: identifier.identifier.text,+            typeAnnotation: simpleTypeIdentifier.name.text,+            comment: comment(from: leadingTrivia),+            access: access,+            scope: scope))+          return+        }+        // no type annotation was found. In this case it's much more complicated to get+        // the type information for a variable.+        // TODO: Include logic to get types of any variable declaration

I think it makes sense to not pass the type here, This makes sense! Let's add this as a comment 🙏

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeWithPropInfo? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeWithPropInfo?+    let visitor = TypeSyntaxVisitor() { [unowned self] typeWithPropInfo in+      guard self.typeName == typeWithPropInfo.name else { return }+      result = try? typeWithPropInfo.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeLocationSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(onNodeVisit: @escaping (_ info: TypeWithPropInfo) -> Void) {+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self) {+      processNode(node, withName: typeIdentifier.name.text, members: node.members.members)+    }+    return .visitChildren+  }++  // MARK: Private++  private let onNodeVisit: (_ info: TypeWithPropInfo) -> Void++  private func processNode<Node>(_ node: Node, withName name: String, members: MemberDeclListSyntax) where Node: SyntaxProtocol {+    var result = [TypeWithPropInfo.PropInfo]()+    let propertyVisitor = PropertySyntaxVisitor(parentName: name) { info in+      result.append(info)+    }+    propertyVisitor.walk(node)+    onNodeVisit(.init(name: name, properties: result))+  }+}++// MARK: - PropertySyntaxVisitor++private final class PropertySyntaxVisitor: SyntaxVisitor {++  init(parentName: String, onNodeVisit: @escaping (_ info: TypeWithPropInfo.PropInfo) -> Void) {+    self.parentName = parentName+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {+    var leadingTrivia: Trivia? = node.leadingTrivia+    var access: TypeWithPropInfo.Access = .internal+    var scope: TypeWithPropInfo.Scope = .instance++    node.modifiers?.forEach { modifier in+      // leading trivia is on the first modifier, or the node itself if no modifiers are present+      leadingTrivia = leadingTrivia ?? modifier.leadingTrivia+      // This attempts to get any access information from the node's modifiers+      if let modifierAccess = TypeWithPropInfo.Access(rawValue: modifier.name.text) {+        access = modifierAccess+      }+      // this attempts to get any scope information from the node's modifiers+      if let modifierScope = TypeWithPropInfo.Scope(rawValue: modifier.name.text) {+        scope = modifierScope+      }+    }++    // in practice this `bindings` array only has 1 element

I don't think so. That'd be one variable declaration IIUC with two bindings but I haven't validated this. maybe we can add a test for this use case?

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 3/27/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick+import Foundation++@testable import SwiftInspectorCore++final class PropertyAnalyzerSpec: QuickSpec {+  override func spec() {+    var fileURL: URL!+    var sut = PropertyAnalyzer(typeName: "FakeType")++    beforeEach {+      sut = PropertyAnalyzer(typeName: "FakeType")+    }++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }++    describe("TypeWithPropInfo.merge(other:)") {+      let type1 = TypeWithPropInfo(+        name: "MyType",+        properties: [+          TypeWithPropInfo.PropInfo(name: "thing", typeAnnotation: "String", comment: "", access: .public, scope: .instance)+      ])+      let type2 = TypeWithPropInfo(+        name: "MyType",+        properties: [+          TypeWithPropInfo.PropInfo(name: "foo", typeAnnotation: "Int", comment: "", access: .public, scope: .instance)+      ])+      let type3 = TypeWithPropInfo(+        name: "AnotherType",+        properties: [+          TypeWithPropInfo.PropInfo(name: "foo", typeAnnotation: "Int", comment: "", access: .public, scope: .instance)+      ])++      context("when both types have the same name") {+        let result = try? type1.merge(with: type2)+        it("succeeds") {+          expect(result?.name) == "MyType"+        }++        it("has merged props") {+          let set = Set(result?.properties ?? [])+          let expectedSet: Set<TypeWithPropInfo.PropInfo> = [+            TypeWithPropInfo.PropInfo(name: "thing", typeAnnotation: "String", comment: "", access: .public, scope: .instance),+            TypeWithPropInfo.PropInfo(name: "foo", typeAnnotation: "Int", comment: "", access: .public, scope: .instance)+          ]+          expect(set) == expectedSet+        }+      }++      context("when the types don't match") {+        it("fails to merge and asserts") {+          expect { try type1.merge(with: type3) }.to(throwError())+        }+      }++      context("when the other type is nil") {+        it("returns the original") {+          expect(try? type1.merge(with: nil)) == type1+        }+      }+    }++    describe("analyze(fileURL:)") {+      context("when there are no properties") {+        beforeEach {+          let content = """+                        public final class FakeType {}+                        """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns type info with empty property list") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties).to(beEmpty())+        }+      }++      context("when there is a property") {+        beforeEach {+          let content = """+          public final class FakeType {+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("returns nil if the type name is not present") {+          let sut = PropertyAnalyzer(typeName: "AnotherType")+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).to(beNil())+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .public,+              scope: .instance)+          ]+        }+      }++      context("when there is a property (struct)") {+        beforeEach {+          let content = """+          public struct FakeType {+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .public,+              scope: .instance)+          ]+        }+      }++      context("when there is a property (enum)") {+        beforeEach {+          let content = """+          public enum FakeType {+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .public,+              scope: .instance)+          ]+        }+      }++      context("when there is a property (protocol)") {+        beforeEach {+          let content = """+          public protocol FakeType {+            var thing: String { get }+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .internal,+              scope: .instance)+          ]+        }+      }++      context("when there is a property extension") {+        beforeEach {+          let content = """+          public class FakeType { }++          extension FakeType {+            var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .internal,+              scope: .instance)+          ]+        }+      }++      context("when there are multiple properites") {+        beforeEach {+          let content = """+          public final class FakeType {+            public var thing: String = "Hello, World"+            var foo: Int = 4+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          let propSet = Set(result?.properties ?? [])+          let expectedPropSet: Set<TypeWithPropInfo.PropInfo> = [+            .init(name: "thing", typeAnnotation: "String", comment: "", access: .public, scope: .instance),+            .init(name: "foo", typeAnnotation: "Int", comment: "", access: .internal, scope: .instance)+          ]+          expect(propSet) == expectedPropSet+        }+      }++      context("when there is a static property") {+        beforeEach {+          let content = """+          public final class FakeType {+            public static var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .public,+              scope: .static)+          ]+        }+      }++      context("when there is a private property") {

Let's also add a test for wwhen these are the other way: static private instead of private static

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeWithPropInfo? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeWithPropInfo?+    let visitor = TypeSyntaxVisitor() { [unowned self] typeWithPropInfo in+      guard self.typeName == typeWithPropInfo.name else { return }+      result = try? typeWithPropInfo.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeLocationSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(onNodeVisit: @escaping (_ info: TypeWithPropInfo) -> Void) {+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self) {+      processNode(node, withName: typeIdentifier.name.text, members: node.members.members)+    }+    return .visitChildren+  }++  // MARK: Private++  private let onNodeVisit: (_ info: TypeWithPropInfo) -> Void++  private func processNode<Node>(_ node: Node, withName name: String, members: MemberDeclListSyntax) where Node: SyntaxProtocol {+    var result = [TypeWithPropInfo.PropInfo]()+    let propertyVisitor = PropertySyntaxVisitor(parentName: name) { info in+      result.append(info)+    }+    propertyVisitor.walk(node)+    onNodeVisit(.init(name: name, properties: result))+  }+}++// MARK: - PropertySyntaxVisitor++private final class PropertySyntaxVisitor: SyntaxVisitor {++  init(parentName: String, onNodeVisit: @escaping (_ info: TypeWithPropInfo.PropInfo) -> Void) {+    self.parentName = parentName+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {+    var leadingTrivia: Trivia? = node.leadingTrivia+    var access: TypeWithPropInfo.Access = .internal+    var scope: TypeWithPropInfo.Scope = .instance++    node.modifiers?.forEach { modifier in+      // leading trivia is on the first modifier, or the node itself if no modifiers are present+      leadingTrivia = leadingTrivia ?? modifier.leadingTrivia+      // This attempts to get any access information from the node's modifiers+      if let modifierAccess = TypeWithPropInfo.Access(rawValue: modifier.name.text) {+        access = modifierAccess+      }+      // this attempts to get any scope information from the node's modifiers+      if let modifierScope = TypeWithPropInfo.Scope(rawValue: modifier.name.text) {+        scope = modifierScope+      }+    }++    // in practice this `bindings` array only has 1 element+    node.bindings.forEach { binding in+      if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {+        if+          let typeAnnotation = binding.typeAnnotation,+          let simpleTypeIdentifier = typeAnnotation.type.as(SimpleTypeIdentifierSyntax.self)+        {+          onNodeVisit(.init(+            name: identifier.identifier.text,+            typeAnnotation: simpleTypeIdentifier.name.text,+            comment: comment(from: leadingTrivia),+            access: access,+            scope: scope))+          return+        }+        // no type annotation was found. In this case it's much more complicated to get+        // the type information for a variable.+        // TODO: Include logic to get types of any variable declaration+        onNodeVisit(.init(+          name: identifier.identifier.text,+          typeAnnotation: nil,+          comment: comment(from: leadingTrivia),+          access: access,+          scope: scope))+      }+    }++    return .skipChildren+  }++  // MARK: Private++  private let parentName: String

nit: typeName?

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 3/27/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick+import Foundation++@testable import SwiftInspectorCore++final class PropertyAnalyzerSpec: QuickSpec {+  override func spec() {+    var fileURL: URL!+    var sut = PropertyAnalyzer(typeName: "FakeType")++    beforeEach {+      sut = PropertyAnalyzer(typeName: "FakeType")+    }++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }++    describe("TypeWithPropInfo.merge(other:)") {+      let type1 = TypeWithPropInfo(+        name: "MyType",+        properties: [+          TypeWithPropInfo.PropInfo(name: "thing", typeAnnotation: "String", comment: "", access: .public, scope: .instance)+      ])+      let type2 = TypeWithPropInfo(+        name: "MyType",+        properties: [+          TypeWithPropInfo.PropInfo(name: "foo", typeAnnotation: "Int", comment: "", access: .public, scope: .instance)+      ])+      let type3 = TypeWithPropInfo(+        name: "AnotherType",+        properties: [+          TypeWithPropInfo.PropInfo(name: "foo", typeAnnotation: "Int", comment: "", access: .public, scope: .instance)+      ])++      context("when both types have the same name") {+        let result = try? type1.merge(with: type2)+        it("succeeds") {+          expect(result?.name) == "MyType"+        }++        it("has merged props") {+          let set = Set(result?.properties ?? [])+          let expectedSet: Set<TypeWithPropInfo.PropInfo> = [+            TypeWithPropInfo.PropInfo(name: "thing", typeAnnotation: "String", comment: "", access: .public, scope: .instance),+            TypeWithPropInfo.PropInfo(name: "foo", typeAnnotation: "Int", comment: "", access: .public, scope: .instance)+          ]+          expect(set) == expectedSet+        }+      }++      context("when the types don't match") {+        it("fails to merge and asserts") {+          expect { try type1.merge(with: type3) }.to(throwError())+        }+      }++      context("when the other type is nil") {+        it("returns the original") {+          expect(try? type1.merge(with: nil)) == type1+        }+      }+    }++    describe("analyze(fileURL:)") {+      context("when there are no properties") {+        beforeEach {+          let content = """+                        public final class FakeType {}+                        """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns type info with empty property list") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties).to(beEmpty())+        }+      }++      context("when there is a property") {+        beforeEach {+          let content = """+          public final class FakeType {+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("returns nil if the type name is not present") {+          let sut = PropertyAnalyzer(typeName: "AnotherType")+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).to(beNil())+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .public,+              scope: .instance)+          ]+        }+      }++      context("when there is a property (struct)") {+        beforeEach {+          let content = """+          public struct FakeType {+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .public,+              scope: .instance)+          ]+        }+      }++      context("when there is a property (enum)") {+        beforeEach {+          let content = """+          public enum FakeType {+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .public,+              scope: .instance)+          ]+        }+      }++      context("when there is a property (protocol)") {+        beforeEach {+          let content = """+          public protocol FakeType {+            var thing: String { get }+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .internal,+              scope: .instance)+          ]+        }+      }++      context("when there is a property extension") {+        beforeEach {+          let content = """+          public class FakeType { }++          extension FakeType {+            var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the type name") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.name) == "FakeType"+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .internal,+              scope: .instance)+          ]+        }+      }++      context("when there are multiple properites") {+        beforeEach {+          let content = """+          public final class FakeType {+            public var thing: String = "Hello, World"+            var foo: Int = 4+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the properties") {+          let result = try? sut.analyze(fileURL: fileURL)+          let propSet = Set(result?.properties ?? [])+          let expectedPropSet: Set<TypeWithPropInfo.PropInfo> = [+            .init(name: "thing", typeAnnotation: "String", comment: "", access: .public, scope: .instance),+            .init(name: "foo", typeAnnotation: "Int", comment: "", access: .internal, scope: .instance)+          ]+          expect(propSet) == expectedPropSet+        }+      }++      context("when there is a static property") {+        beforeEach {+          let content = """+          public final class FakeType {+            public static var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .public,+              scope: .static)+          ]+        }+      }++      context("when there is a private property") {+        beforeEach {+          let content = """+          public final class FakeType {+            private static var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .private,+              scope: .static)+          ]+        }+      }++      context("when there is a fileprivate property") {+        beforeEach {+          let content = """+          public final class FakeType {+            fileprivate static var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "",+              access: .fileprivate,+              scope: .static)+          ]+        }+      }++      context("when there is no type annotation") {+        beforeEach {+          let content = """+          public final class FakeType {+            private static var thing = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property with no type information") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: nil,+              comment: "",+              access: .private,+              scope: .static)+          ]+        }+      }++      context("when there is a line comment") {+        beforeEach {+          let content = """+          public final class FakeType {+            // The thing+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property with the comment") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "// The thing",+              access: .public,+              scope: .instance)+          ]+        }+      }++      context("when there is a doc line comment") {+        beforeEach {+          let content = """+          public final class FakeType {+            /// The thing+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property with the comment") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "/// The thing",+              access: .public,+              scope: .instance)+          ]+        }+      }++      context("when there is a block comment") {+        beforeEach {+          let content = """+          public final class FakeType {+            /* The thing */+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property with the comment") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "/* The thing */",+              access: .public,+              scope: .instance)+          ]+        }+      }++      context("when there is a doc block comment") {+        beforeEach {+          let content = """+          public final class FakeType {+            /** The thing */+            public var thing: String = "Hello, World"+          }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("detects the property with the comment") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result?.properties) == [+            TypeWithPropInfo.PropInfo(+              name: "thing",+              typeAnnotation: "String",+              comment: "/** The thing */",+              access: .public,+              scope: .instance)+          ]+        }+      }

Let's add a test for the public private(set) case too

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeWithPropInfo? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeWithPropInfo?+    let visitor = TypeSyntaxVisitor() { [unowned self] typeWithPropInfo in+      guard self.typeName == typeWithPropInfo.name else { return }+      result = try? typeWithPropInfo.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeLocationSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(onNodeVisit: @escaping (_ info: TypeWithPropInfo) -> Void) {+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self) {+      processNode(node, withName: typeIdentifier.name.text, members: node.members.members)+    }+    return .visitChildren+  }++  // MARK: Private++  private let onNodeVisit: (_ info: TypeWithPropInfo) -> Void++  private func processNode<Node>(_ node: Node, withName name: String, members: MemberDeclListSyntax) where Node: SyntaxProtocol {+    var result = [TypeWithPropInfo.PropInfo]()+    let propertyVisitor = PropertySyntaxVisitor(parentName: name) { info in+      result.append(info)+    }+    propertyVisitor.walk(node)+    onNodeVisit(.init(name: name, properties: result))+  }+}++// MARK: - PropertySyntaxVisitor++private final class PropertySyntaxVisitor: SyntaxVisitor {++  init(parentName: String, onNodeVisit: @escaping (_ info: TypeWithPropInfo.PropInfo) -> Void) {+    self.parentName = parentName+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {+    var leadingTrivia: Trivia? = node.leadingTrivia+    var access: TypeWithPropInfo.Access = .internal+    var scope: TypeWithPropInfo.Scope = .instance++    node.modifiers?.forEach { modifier in+      // leading trivia is on the first modifier, or the node itself if no modifiers are present+      leadingTrivia = leadingTrivia ?? modifier.leadingTrivia+      // This attempts to get any access information from the node's modifiers+      if let modifierAccess = TypeWithPropInfo.Access(rawValue: modifier.name.text) {

Should we be looking into DeclModifier syntax instead? I think this could break if we have an attribute like public private(set) since we'd be first assigning the modifier to public, but later re-assigning to private.

I think it'd be better to look at the tokens of each declaration modifier and look for the public, internal, etc keywords, then we could validate that we only have 1 token (so we make sure it's only public and not something like public(set)`?

I've found that assuming ordering when using the AST usually ends up biting me back. I think using the token names tends to be more robust and scales better.

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/17/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 ArgumentParser+import Foundation+import SwiftInspectorCore++final class PropertyAnalyzerCommand: ParsableCommand {+  static var configuration = CommandConfiguration(+    commandName: "properties",+    abstract: "Finds property information for the provided type"+  )++  @Option(help: nameArgumentHelp)+  var name: String++  @Option(help: "The absolute path of the file to inspect")+  var path: String++  /// Runs the command+  func run() throws {+    let cachedSyntaxTree = CachedSyntaxTree()++    let analyzer = PropertyAnalyzer(typeName: name, cachedSyntaxTree: cachedSyntaxTree)+    let fileURL = URL(fileURLWithPath: path)+    if let propertyInformation = try analyzer.analyze(fileURL: fileURL) {+      print(outputString(from: propertyInformation))+    }+  }++  /// Validates if the arguments of this command are valid+  func validate() throws {+    guard !name.isEmpty else {+      throw InspectorError.emptyArgument(argumentName: "--name")+    }+    guard !path.isEmpty else {+      throw InspectorError.emptyArgument(argumentName: "--path")+    }+    let pathURL = URL(fileURLWithPath: path)+    guard FileManager.default.isSwiftFile(at: pathURL) else {+      throw InspectorError.invalidArgument(argumentName: "--path", value: path)+    }+  }++  private func outputString(from statement: TypeWithPropInfo) -> String {+    let propString = statement.properties.map { propInfo in+      "(\(propInfo.name)|\(propInfo.access)|\(propInfo.scope))"

Should we use spaces or commas to separate these and then new lines for each property instead? this would be the first command that uses| to separate fields

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeWithPropInfo? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeWithPropInfo?+    let visitor = TypeSyntaxVisitor() { [unowned self] typeWithPropInfo in+      guard self.typeName == typeWithPropInfo.name else { return }+      result = try? typeWithPropInfo.merge(with: result)

Should we rethrow this error instead of ignoring it?

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeWithPropInfo? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeWithPropInfo?+    let visitor = TypeSyntaxVisitor() { [unowned self] typeWithPropInfo in+      guard self.typeName == typeWithPropInfo.name else { return }+      result = try? typeWithPropInfo.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeLocationSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(onNodeVisit: @escaping (_ info: TypeWithPropInfo) -> Void) {+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self) {+      processNode(node, withName: typeIdentifier.name.text, members: node.members.members)+    }+    return .visitChildren+  }++  // MARK: Private++  private let onNodeVisit: (_ info: TypeWithPropInfo) -> Void++  private func processNode<Node>(_ node: Node, withName name: String, members: MemberDeclListSyntax) where Node: SyntaxProtocol {+    var result = [TypeWithPropInfo.PropInfo]()+    let propertyVisitor = PropertySyntaxVisitor(parentName: name) { info in+      result.append(info)+    }+    propertyVisitor.walk(node)+    onNodeVisit(.init(name: name, properties: result))+  }+}++// MARK: - PropertySyntaxVisitor++private final class PropertySyntaxVisitor: SyntaxVisitor {++  init(parentName: String, onNodeVisit: @escaping (_ info: TypeWithPropInfo.PropInfo) -> Void) {+    self.parentName = parentName+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {+    var leadingTrivia: Trivia? = node.leadingTrivia+    var access: TypeWithPropInfo.Access = .internal+    var scope: TypeWithPropInfo.Scope = .instance++    node.modifiers?.forEach { modifier in+      // leading trivia is on the first modifier, or the node itself if no modifiers are present+      leadingTrivia = leadingTrivia ?? modifier.leadingTrivia+      // This attempts to get any access information from the node's modifiers+      if let modifierAccess = TypeWithPropInfo.Access(rawValue: modifier.name.text) {+        access = modifierAccess+      }+      // this attempts to get any scope information from the node's modifiers+      if let modifierScope = TypeWithPropInfo.Scope(rawValue: modifier.name.text) {+        scope = modifierScope+      }+    }++    // in practice this `bindings` array only has 1 element+    node.bindings.forEach { binding in+      if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {+        if+          let typeAnnotation = binding.typeAnnotation,+          let simpleTypeIdentifier = typeAnnotation.type.as(SimpleTypeIdentifierSyntax.self)+        {+          onNodeVisit(.init(+            name: identifier.identifier.text,+            typeAnnotation: simpleTypeIdentifier.name.text,+            comment: comment(from: leadingTrivia),+            access: access,+            scope: scope))+          return+        }+        // no type annotation was found. In this case it's much more complicated to get+        // the type information for a variable.+        // TODO: Include logic to get types of any variable declaration

What's an example of this use case?

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeWithPropInfo? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeWithPropInfo?+    let visitor = TypeSyntaxVisitor() { [unowned self] typeWithPropInfo in+      guard self.typeName == typeWithPropInfo.name else { return }+      result = try? typeWithPropInfo.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeLocationSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(onNodeVisit: @escaping (_ info: TypeWithPropInfo) -> Void) {+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self) {+      processNode(node, withName: typeIdentifier.name.text, members: node.members.members)+    }+    return .visitChildren+  }++  // MARK: Private++  private let onNodeVisit: (_ info: TypeWithPropInfo) -> Void++  private func processNode<Node>(_ node: Node, withName name: String, members: MemberDeclListSyntax) where Node: SyntaxProtocol {+    var result = [TypeWithPropInfo.PropInfo]()+    let propertyVisitor = PropertySyntaxVisitor(parentName: name) { info in+      result.append(info)+    }+    propertyVisitor.walk(node)+    onNodeVisit(.init(name: name, properties: result))+  }+}++// MARK: - PropertySyntaxVisitor++private final class PropertySyntaxVisitor: SyntaxVisitor {++  init(parentName: String, onNodeVisit: @escaping (_ info: TypeWithPropInfo.PropInfo) -> Void) {+    self.parentName = parentName+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {+    var leadingTrivia: Trivia? = node.leadingTrivia+    var access: TypeWithPropInfo.Access = .internal+    var scope: TypeWithPropInfo.Scope = .instance++    node.modifiers?.forEach { modifier in+      // leading trivia is on the first modifier, or the node itself if no modifiers are present+      leadingTrivia = leadingTrivia ?? modifier.leadingTrivia+      // This attempts to get any access information from the node's modifiers+      if let modifierAccess = TypeWithPropInfo.Access(rawValue: modifier.name.text) {+        access = modifierAccess+      }+      // this attempts to get any scope information from the node's modifiers+      if let modifierScope = TypeWithPropInfo.Scope(rawValue: modifier.name.text) {+        scope = modifierScope+      }+    }++    // in practice this `bindings` array only has 1 element

i don't think this is always true, I think we can have something like:

struct Foo {
  let some: String, another: Int
}

And this will contain 2 bindings IIUC

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeWithPropInfo? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeWithPropInfo?+    let visitor = TypeSyntaxVisitor() { [unowned self] typeWithPropInfo in+      guard self.typeName == typeWithPropInfo.name else { return }+      result = try? typeWithPropInfo.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeLocationSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(onNodeVisit: @escaping (_ info: TypeWithPropInfo) -> Void) {+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self) {+      processNode(node, withName: typeIdentifier.name.text, members: node.members.members)+    }+    return .visitChildren+  }++  // MARK: Private++  private let onNodeVisit: (_ info: TypeWithPropInfo) -> Void++  private func processNode<Node>(_ node: Node, withName name: String, members: MemberDeclListSyntax) where Node: SyntaxProtocol {+    var result = [TypeWithPropInfo.PropInfo]()+    let propertyVisitor = PropertySyntaxVisitor(parentName: name) { info in+      result.append(info)+    }+    propertyVisitor.walk(node)+    onNodeVisit(.init(name: name, properties: result))+  }+}++// MARK: - PropertySyntaxVisitor++private final class PropertySyntaxVisitor: SyntaxVisitor {++  init(parentName: String, onNodeVisit: @escaping (_ info: TypeWithPropInfo.PropInfo) -> Void) {+    self.parentName = parentName+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {+    var leadingTrivia: Trivia? = node.leadingTrivia+    var access: TypeWithPropInfo.Access = .internal+    var scope: TypeWithPropInfo.Scope = .instance++    node.modifiers?.forEach { modifier in+      // leading trivia is on the first modifier, or the node itself if no modifiers are present+      leadingTrivia = leadingTrivia ?? modifier.leadingTrivia+      // This attempts to get any access information from the node's modifiers+      if let modifierAccess = TypeWithPropInfo.Access(rawValue: modifier.name.text) {+        access = modifierAccess+      }+      // this attempts to get any scope information from the node's modifiers+      if let modifierScope = TypeWithPropInfo.Scope(rawValue: modifier.name.text) {

same here, probably better to look for the static keyword in the tokens of the Decl modifier

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeWithPropInfo? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeWithPropInfo?+    let visitor = TypeSyntaxVisitor() { [unowned self] typeWithPropInfo in+      guard self.typeName == typeWithPropInfo.name else { return }+      result = try? typeWithPropInfo.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeLocationSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(onNodeVisit: @escaping (_ info: TypeWithPropInfo) -> Void) {+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self) {

Can you explain why we need this casting?

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeWithPropInfo? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeWithPropInfo?+    let visitor = TypeSyntaxVisitor() { [unowned self] typeWithPropInfo in+      guard self.typeName == typeWithPropInfo.name else { return }+      result = try? typeWithPropInfo.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeLocationSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(onNodeVisit: @escaping (_ info: TypeWithPropInfo) -> Void) {+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)

Should we validate in here the name instead that after doing all the processing? That will likely improve the performance of this tool (since we won't be processing every type, only the types we care about)

I'mm thinking we should pass the type name to the TypeSyntaxVisitor and if the name is not the one expected on these methods we can do a guard and visit children immediately, and only process the node when the type name is the one we care about.

guard node.identifier.text == typeName else { return .visitChildren }
thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeWithPropInfo? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeWithPropInfo?+    let visitor = TypeSyntaxVisitor() { [unowned self] typeWithPropInfo in+      guard self.typeName == typeWithPropInfo.name else { return }+      result = try? typeWithPropInfo.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeLocationSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(onNodeVisit: @escaping (_ info: TypeWithPropInfo) -> Void) {+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self) {+      processNode(node, withName: typeIdentifier.name.text, members: node.members.members)+    }+    return .visitChildren+  }++  // MARK: Private++  private let onNodeVisit: (_ info: TypeWithPropInfo) -> Void++  private func processNode<Node>(_ node: Node, withName name: String, members: MemberDeclListSyntax) where Node: SyntaxProtocol {+    var result = [TypeWithPropInfo.PropInfo]()+    let propertyVisitor = PropertySyntaxVisitor(parentName: name) { info in+      result.append(info)+    }+    propertyVisitor.walk(node)+    onNodeVisit(.init(name: name, properties: result))+  }+}++// MARK: - PropertySyntaxVisitor++private final class PropertySyntaxVisitor: SyntaxVisitor {++  init(parentName: String, onNodeVisit: @escaping (_ info: TypeWithPropInfo.PropInfo) -> Void) {+    self.parentName = parentName+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {+    var leadingTrivia: Trivia? = node.leadingTrivia+    var access: TypeWithPropInfo.Access = .internal+    var scope: TypeWithPropInfo.Scope = .instance++    node.modifiers?.forEach { modifier in+      // leading trivia is on the first modifier, or the node itself if no modifiers are present+      leadingTrivia = leadingTrivia ?? modifier.leadingTrivia+      // This attempts to get any access information from the node's modifiers+      if let modifierAccess = TypeWithPropInfo.Access(rawValue: modifier.name.text) {+        access = modifierAccess+      }+      // this attempts to get any scope information from the node's modifiers+      if let modifierScope = TypeWithPropInfo.Scope(rawValue: modifier.name.text) {+        scope = modifierScope+      }+    }++    // in practice this `bindings` array only has 1 element+    node.bindings.forEach { binding in+      if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {+        if+          let typeAnnotation = binding.typeAnnotation,+          let simpleTypeIdentifier = typeAnnotation.type.as(SimpleTypeIdentifierSyntax.self)+        {+          onNodeVisit(.init(+            name: identifier.identifier.text,+            typeAnnotation: simpleTypeIdentifier.name.text,+            comment: comment(from: leadingTrivia),+            access: access,+            scope: scope))+          return+        }+        // no type annotation was found. In this case it's much more complicated to get+        // the type information for a variable.+        // TODO: Include logic to get types of any variable declaration+        onNodeVisit(.init(+          name: identifier.identifier.text,+          typeAnnotation: nil,+          comment: comment(from: leadingTrivia),+          access: access,+          scope: scope))+      }+    }++    return .skipChildren+  }++  // MARK: Private++  private let parentName: String+  private let onNodeVisit: (_ info: TypeWithPropInfo.PropInfo) -> Void++  private func comment(from trivia: Trivia?) -> String {+    guard let trivia = trivia else { return "" }+    return trivia.compactMap { piece -> String? in+      switch piece {+      case .lineComment(let str): return str+      case .blockComment(let str): return str+      case .docLineComment(let str): return str+      case .docBlockComment(let str): return str+      default: return nil+      }+    }.joined(separator: "\n")+  }+}++// MARK: - TypeWithPropInfo++/// Information about a located type. Indices start with 0.

I don't think this comment is correct?

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeWithPropInfo? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeWithPropInfo?+    let visitor = TypeSyntaxVisitor() { [unowned self] typeWithPropInfo in+      guard self.typeName == typeWithPropInfo.name else { return }+      result = try? typeWithPropInfo.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeLocationSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(onNodeVisit: @escaping (_ info: TypeWithPropInfo) -> Void) {+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self) {+      processNode(node, withName: typeIdentifier.name.text, members: node.members.members)+    }+    return .visitChildren+  }++  // MARK: Private++  private let onNodeVisit: (_ info: TypeWithPropInfo) -> Void++  private func processNode<Node>(_ node: Node, withName name: String, members: MemberDeclListSyntax) where Node: SyntaxProtocol {+    var result = [TypeWithPropInfo.PropInfo]()+    let propertyVisitor = PropertySyntaxVisitor(parentName: name) { info in+      result.append(info)+    }+    propertyVisitor.walk(node)+    onNodeVisit(.init(name: name, properties: result))+  }+}++// MARK: - PropertySyntaxVisitor++private final class PropertySyntaxVisitor: SyntaxVisitor {++  init(parentName: String, onNodeVisit: @escaping (_ info: TypeWithPropInfo.PropInfo) -> Void) {+    self.parentName = parentName+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {+    var leadingTrivia: Trivia? = node.leadingTrivia+    var access: TypeWithPropInfo.Access = .internal+    var scope: TypeWithPropInfo.Scope = .instance++    node.modifiers?.forEach { modifier in+      // leading trivia is on the first modifier, or the node itself if no modifiers are present+      leadingTrivia = leadingTrivia ?? modifier.leadingTrivia+      // This attempts to get any access information from the node's modifiers+      if let modifierAccess = TypeWithPropInfo.Access(rawValue: modifier.name.text) {+        access = modifierAccess+      }+      // this attempts to get any scope information from the node's modifiers+      if let modifierScope = TypeWithPropInfo.Scope(rawValue: modifier.name.text) {+        scope = modifierScope+      }+    }++    // in practice this `bindings` array only has 1 element+    node.bindings.forEach { binding in+      if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {+        if+          let typeAnnotation = binding.typeAnnotation,+          let simpleTypeIdentifier = typeAnnotation.type.as(SimpleTypeIdentifierSyntax.self)+        {+          onNodeVisit(.init(+            name: identifier.identifier.text,+            typeAnnotation: simpleTypeIdentifier.name.text,+            comment: comment(from: leadingTrivia),+            access: access,+            scope: scope))+          return+        }+        // no type annotation was found. In this case it's much more complicated to get+        // the type information for a variable.+        // TODO: Include logic to get types of any variable declaration+        onNodeVisit(.init(+          name: identifier.identifier.text,+          typeAnnotation: nil,+          comment: comment(from: leadingTrivia),+          access: access,+          scope: scope))+      }+    }++    return .skipChildren+  }++  // MARK: Private++  private let parentName: String+  private let onNodeVisit: (_ info: TypeWithPropInfo.PropInfo) -> Void++  private func comment(from trivia: Trivia?) -> String {+    guard let trivia = trivia else { return "" }+    return trivia.compactMap { piece -> String? in+      switch piece {+      case .lineComment(let str): return str+      case .blockComment(let str): return str+      case .docLineComment(let str): return str+      case .docBlockComment(let str): return str+      default: return nil+      }+    }.joined(separator: "\n")+  }+}++// MARK: - TypeWithPropInfo++/// Information about a located type. Indices start with 0.+public struct TypeWithPropInfo: Hashable {++  public enum Access: String {+    case `public`+    case `internal`+    case `private`+    case `fileprivate`+  }++  public enum Scope: String {+    case instance+    case `static`+  }++  public struct PropInfo: Hashable {

PropertyInfo https://swift.org/documentation/api-design-guidelines/#promote-clear-usage ?

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeWithPropInfo? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeWithPropInfo?+    let visitor = TypeSyntaxVisitor() { [unowned self] typeWithPropInfo in+      guard self.typeName == typeWithPropInfo.name else { return }+      result = try? typeWithPropInfo.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeLocationSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(onNodeVisit: @escaping (_ info: TypeWithPropInfo) -> Void) {+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self) {+      processNode(node, withName: typeIdentifier.name.text, members: node.members.members)+    }+    return .visitChildren+  }++  // MARK: Private++  private let onNodeVisit: (_ info: TypeWithPropInfo) -> Void++  private func processNode<Node>(_ node: Node, withName name: String, members: MemberDeclListSyntax) where Node: SyntaxProtocol {+    var result = [TypeWithPropInfo.PropInfo]()+    let propertyVisitor = PropertySyntaxVisitor(parentName: name) { info in+      result.append(info)+    }+    propertyVisitor.walk(node)+    onNodeVisit(.init(name: name, properties: result))+  }+}++// MARK: - PropertySyntaxVisitor++private final class PropertySyntaxVisitor: SyntaxVisitor {++  init(parentName: String, onNodeVisit: @escaping (_ info: TypeWithPropInfo.PropInfo) -> Void) {+    self.parentName = parentName+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {+    var leadingTrivia: Trivia? = node.leadingTrivia+    var access: TypeWithPropInfo.Access = .internal+    var scope: TypeWithPropInfo.Scope = .instance++    node.modifiers?.forEach { modifier in+      // leading trivia is on the first modifier, or the node itself if no modifiers are present+      leadingTrivia = leadingTrivia ?? modifier.leadingTrivia+      // This attempts to get any access information from the node's modifiers+      if let modifierAccess = TypeWithPropInfo.Access(rawValue: modifier.name.text) {+        access = modifierAccess+      }+      // this attempts to get any scope information from the node's modifiers+      if let modifierScope = TypeWithPropInfo.Scope(rawValue: modifier.name.text) {+        scope = modifierScope+      }+    }++    // in practice this `bindings` array only has 1 element+    node.bindings.forEach { binding in+      if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {+        if+          let typeAnnotation = binding.typeAnnotation,+          let simpleTypeIdentifier = typeAnnotation.type.as(SimpleTypeIdentifierSyntax.self)+        {+          onNodeVisit(.init(+            name: identifier.identifier.text,+            typeAnnotation: simpleTypeIdentifier.name.text,+            comment: comment(from: leadingTrivia),+            access: access,+            scope: scope))+          return+        }+        // no type annotation was found. In this case it's much more complicated to get+        // the type information for a variable.+        // TODO: Include logic to get types of any variable declaration+        onNodeVisit(.init(+          name: identifier.identifier.text,+          typeAnnotation: nil,+          comment: comment(from: leadingTrivia),+          access: access,+          scope: scope))+      }+    }++    return .skipChildren+  }++  // MARK: Private++  private let parentName: String+  private let onNodeVisit: (_ info: TypeWithPropInfo.PropInfo) -> Void++  private func comment(from trivia: Trivia?) -> String {+    guard let trivia = trivia else { return "" }+    return trivia.compactMap { piece -> String? in+      switch piece {+      case .lineComment(let str): return str+      case .blockComment(let str): return str+      case .docLineComment(let str): return str+      case .docBlockComment(let str): return str+      default: return nil+      }+    }.joined(separator: "\n")+  }+}++// MARK: - TypeWithPropInfo++/// Information about a located type. Indices start with 0.+public struct TypeWithPropInfo: Hashable {++  public enum Access: String {+    case `public`+    case `internal`+    case `private`+    case `fileprivate`+  }++  public enum Scope: String {+    case instance+    case `static`+  }++  public struct PropInfo: Hashable {+    /// The name of the property+    public let name: String+    /// The Type annotation of the property if it's present+    public let typeAnnotation: String?+    /// Any comments associated with the property+    public let comment: String+    /// Access control of this property+    public let access: Access+    /// Scope of this property+    public let scope: Scope+  }++  /// The name of the type.+  public let name: String+  /// The properites on this type

typo: properties

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeWithPropInfo? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeWithPropInfo?+    let visitor = TypeSyntaxVisitor() { [unowned self] typeWithPropInfo in+      guard self.typeName == typeWithPropInfo.name else { return }+      result = try? typeWithPropInfo.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeLocationSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(onNodeVisit: @escaping (_ info: TypeWithPropInfo) -> Void) {+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self) {+      processNode(node, withName: typeIdentifier.name.text, members: node.members.members)+    }+    return .visitChildren+  }++  // MARK: Private++  private let onNodeVisit: (_ info: TypeWithPropInfo) -> Void++  private func processNode<Node>(_ node: Node, withName name: String, members: MemberDeclListSyntax) where Node: SyntaxProtocol {+    var result = [TypeWithPropInfo.PropInfo]()+    let propertyVisitor = PropertySyntaxVisitor(parentName: name) { info in+      result.append(info)+    }+    propertyVisitor.walk(node)+    onNodeVisit(.init(name: name, properties: result))+  }+}++// MARK: - PropertySyntaxVisitor++private final class PropertySyntaxVisitor: SyntaxVisitor {++  init(parentName: String, onNodeVisit: @escaping (_ info: TypeWithPropInfo.PropInfo) -> Void) {+    self.parentName = parentName+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {+    var leadingTrivia: Trivia? = node.leadingTrivia+    var access: TypeWithPropInfo.Access = .internal+    var scope: TypeWithPropInfo.Scope = .instance++    node.modifiers?.forEach { modifier in+      // leading trivia is on the first modifier, or the node itself if no modifiers are present+      leadingTrivia = leadingTrivia ?? modifier.leadingTrivia+      // This attempts to get any access information from the node's modifiers+      if let modifierAccess = TypeWithPropInfo.Access(rawValue: modifier.name.text) {+        access = modifierAccess+      }+      // this attempts to get any scope information from the node's modifiers+      if let modifierScope = TypeWithPropInfo.Scope(rawValue: modifier.name.text) {+        scope = modifierScope+      }+    }++    // in practice this `bindings` array only has 1 element+    node.bindings.forEach { binding in+      if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {+        if+          let typeAnnotation = binding.typeAnnotation,+          let simpleTypeIdentifier = typeAnnotation.type.as(SimpleTypeIdentifierSyntax.self)+        {+          onNodeVisit(.init(+            name: identifier.identifier.text,+            typeAnnotation: simpleTypeIdentifier.name.text,+            comment: comment(from: leadingTrivia),+            access: access,+            scope: scope))+          return+        }+        // no type annotation was found. In this case it's much more complicated to get+        // the type information for a variable.+        // TODO: Include logic to get types of any variable declaration+        onNodeVisit(.init(+          name: identifier.identifier.text,+          typeAnnotation: nil,+          comment: comment(from: leadingTrivia),+          access: access,+          scope: scope))+      }+    }++    return .skipChildren+  }++  // MARK: Private++  private let parentName: String+  private let onNodeVisit: (_ info: TypeWithPropInfo.PropInfo) -> Void++  private func comment(from trivia: Trivia?) -> String {+    guard let trivia = trivia else { return "" }+    return trivia.compactMap { piece -> String? in+      switch piece {+      case .lineComment(let str): return str+      case .blockComment(let str): return str+      case .docLineComment(let str): return str+      case .docBlockComment(let str): return str+      default: return nil+      }+    }.joined(separator: "\n")+  }+}++// MARK: - TypeWithPropInfo++/// Information about a located type. Indices start with 0.+public struct TypeWithPropInfo: Hashable {++  public enum Access: String {

nit: Let's name this AccessControl

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file

I don't think this is correct?

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/14/20.+//+// Distributed under the MIT License+//+// 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++// MARK: - TypeLocationAnalyzer++public final class PropertyAnalyzer: Analyzer {++  /// - Parameter typeName: The name of the type to get property information for+  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.typeName = typeName+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Finds the location(s) of the specified type name in the Swift file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> TypeWithPropInfo? {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result: TypeWithPropInfo?+    let visitor = TypeSyntaxVisitor() { [unowned self] typeWithPropInfo in+      guard self.typeName == typeWithPropInfo.name else { return }+      result = try? typeWithPropInfo.merge(with: result)+    }+    visitor.walk(syntax)+    return result+  }++  // MARK: Private++  private let cachedSyntaxTree: CachedSyntaxTree+  private let typeName: String+}++// MARK: - TypeLocationSyntaxVisitor++private final class TypeSyntaxVisitor: SyntaxVisitor {++  init(onNodeVisit: @escaping (_ info: TypeWithPropInfo) -> Void) {+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {+    processNode(node, withName: node.identifier.text, members: node.members.members)+    return .visitChildren+  }++  override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {+    if let typeIdentifier = node.extendedType.as(SimpleTypeIdentifierSyntax.self) {+      processNode(node, withName: typeIdentifier.name.text, members: node.members.members)+    }+    return .visitChildren+  }++  // MARK: Private++  private let onNodeVisit: (_ info: TypeWithPropInfo) -> Void++  private func processNode<Node>(_ node: Node, withName name: String, members: MemberDeclListSyntax) where Node: SyntaxProtocol {+    var result = [TypeWithPropInfo.PropInfo]()+    let propertyVisitor = PropertySyntaxVisitor(parentName: name) { info in+      result.append(info)+    }+    propertyVisitor.walk(node)+    onNodeVisit(.init(name: name, properties: result))+  }+}++// MARK: - PropertySyntaxVisitor++private final class PropertySyntaxVisitor: SyntaxVisitor {++  init(parentName: String, onNodeVisit: @escaping (_ info: TypeWithPropInfo.PropInfo) -> Void) {+    self.parentName = parentName+    self.onNodeVisit = onNodeVisit+  }++  override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {+    var leadingTrivia: Trivia? = node.leadingTrivia+    var access: TypeWithPropInfo.Access = .internal+    var scope: TypeWithPropInfo.Scope = .instance++    node.modifiers?.forEach { modifier in+      // leading trivia is on the first modifier, or the node itself if no modifiers are present+      leadingTrivia = leadingTrivia ?? modifier.leadingTrivia+      // This attempts to get any access information from the node's modifiers+      if let modifierAccess = TypeWithPropInfo.Access(rawValue: modifier.name.text) {+        access = modifierAccess+      }+      // this attempts to get any scope information from the node's modifiers+      if let modifierScope = TypeWithPropInfo.Scope(rawValue: modifier.name.text) {+        scope = modifierScope+      }+    }++    // in practice this `bindings` array only has 1 element+    node.bindings.forEach { binding in+      if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {+        if+          let typeAnnotation = binding.typeAnnotation,+          let simpleTypeIdentifier = typeAnnotation.type.as(SimpleTypeIdentifierSyntax.self)+        {+          onNodeVisit(.init(+            name: identifier.identifier.text,+            typeAnnotation: simpleTypeIdentifier.name.text,+            comment: comment(from: leadingTrivia),+            access: access,+            scope: scope))+          return+        }+        // no type annotation was found. In this case it's much more complicated to get+        // the type information for a variable.+        // TODO: Include logic to get types of any variable declaration+        onNodeVisit(.init(+          name: identifier.identifier.text,+          typeAnnotation: nil,+          comment: comment(from: leadingTrivia),+          access: access,+          scope: scope))+      }+    }++    return .skipChildren+  }++  // MARK: Private++  private let parentName: String+  private let onNodeVisit: (_ info: TypeWithPropInfo.PropInfo) -> Void++  private func comment(from trivia: Trivia?) -> String {+    guard let trivia = trivia else { return "" }+    return trivia.compactMap { piece -> String? in+      switch piece {+      case .lineComment(let str): return str+      case .blockComment(let str): return str+      case .docLineComment(let str): return str+      case .docBlockComment(let str): return str+      default: return nil+      }+    }.joined(separator: "\n")+  }+}++// MARK: - TypeWithPropInfo++/// Information about a located type. Indices start with 0.+public struct TypeWithPropInfo: Hashable {

TypeProperties instead?

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

add property analyzer

+// Created by Tyler Hedrick on 8/17/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick+import Foundation++@testable import SwiftInspectorCore++final class PropertyAnalyzerCommandSpec: QuickSpec {+  override func spec() {+    describe("run") {++      context("with no arguments") {+        it("fails") {+          let result = try? TestTask.run(withArguments: ["properties"])+          expect(result?.didFail) == true+        }+      }++      context("when path is invalid") {+        it("fails when empty") {+          let result = try? TestTask.run(withArguments: ["properties", "--path", ""])+          expect(result?.didFail) == true+        }++        it("fails when not provided") {+          let result = try? TestTask.run(withArguments: ["properties", "--path"])+          expect(result?.didFail) == true+        }++        it("fails when it doesn't exist") {+          let result = try? TestTask.run(withArguments: ["properties", "--path", "/fake/path"])+          expect(result?.didFail) == true+        }++        it("fails when name is present but the file is invalid") {+          let result = try? TestTask.run(withArguments: ["properties", "--path", "/fake/path", "--name", "SomeType"])+          expect(result?.didFail) == true+        }+      }++      context("when name is passed") {+        var fileURL: URL!++        beforeEach {+          fileURL = try? Temporary.makeFile(content: """+          final class Some {+            public var thing: String = "Hello, World"+          }+          """)+        }++        afterEach {+          try? Temporary.removeItem(at: fileURL)+        }++        it("fails when name is empty") {+          let result = try? TestTask.run(withArguments: ["properties", "--path", fileURL.path, "--name", ""])+          expect(result?.didFail) == true+        }++        it("fails when name is not provided") {+          let result = try? TestTask.run(withArguments: ["properties", "--path", fileURL.path, "--name"])+          expect(result?.didFail) == true+        }++        it("fails when path is empty") {+          let result = try? TestTask.run(withArguments: ["properties", "--name", "Some"])+          expect(result?.didFail) == true+        }++        it("succeeds") {+          let result = try? TestTask.run(withArguments: ["properties", "--path", fileURL.path, "--name", "Some"])+          expect(result?.didSucceed) == true+        }++        it("returns the type name with property information including the name, access, and scope") {+          let result = try? TestTask.run(withArguments: ["properties", "--path", fileURL.path, "--name", "Some"])+          expect(result?.outputMessage).to(contain("Some:(thing|public|instance)"))

I'd suggest outputting something like:

Some thing public instance instead. This is a lot easier to parse as output and to provide as arguments for another tool.

I really need to come up with better documentation for these things.

thedrick

comment created time in a month

push eventfdiaz/SwiftInspector

Tyler Hedrick

commit sha a173d02d773af433cc9e36c5797f58a5f7735445

Add types analyzer (#41) * Add types analyzer for a file * tests * add more tests for TypesCommand * address comments and support directories * remove comments from Command * Update Sources/SwiftInspectorCommands/Tests/TypesCommandSpec.swift Co-authored-by: Francisco Diaz <fdiaz@users.noreply.github.com> Co-authored-by: Francisco Diaz <fdiaz@users.noreply.github.com>

view details

push time in a month

PR merged fdiaz/SwiftInspector

Add types analyzer

This adds an analyzer and a command for finding type information given a file. Here's what the help output looks like:

OVERVIEW: Finds information about types in a file

USAGE: SwiftInspector types --path <path>

OPTIONS:
  --path <path>           The absolute path to the file to inspect 
  -h, --help              Show help information.

This will return data in the following format:

<Type Name>,<Type>

where the following code:

import Foundation

// Foo does foo things
public struct Foo {

}

would output

Foo,struct
+707 -0

2 comments

5 changed files

thedrick

pr closed time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick on 8/13/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick+import Foundation++@testable import SwiftInspectorCore++final class TypesCommandSpec: QuickSpec {+  override func spec() {+    var fileURL: URL!++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }+++    describe("run") {+      context("with no arguments") {+        it("fails") {+          let result = try? TestTask.run(withArguments: ["types"])+          expect(result?.didFail) == true+        }+      }++      context("when path is invalid") {+        it("fails when empty") {+          let result = try? TestTypesCommandTask.run(path: "")+          expect(result?.didFail) == true+        }++        it("fails when it doesn't exist") {+          let result = try? TestTypesCommandTask.run(path: "/fake/path")+          expect(result?.didFail) == true+        }+      }++      context("when path is valid") {+        it("succeeds") {+          fileURL = try? Temporary.makeFile(content: "struct Foo { }")+          let path = fileURL?.path ?? ""+          let result = try? TestTypesCommandTask.run(path: path)+          expect(result?.didSucceed) == true+        }++        it("runs and outputs the result without comment") {+          fileURL = try? Temporary.makeFile(content: """+                                                   // This is a comment+                                                   struct Foo { }+                                                   """)+          let path = fileURL?.path ?? ""+          let result = try? TestTypesCommandTask.run(path: path)+          expect(result?.outputMessage).to(contain("Foo,struct"))

No worries, this was mostly a nit so let's keep it as is 😉

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick on 8/12/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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++public final class TypesAnalyzer: Analyzer {++  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Analyzes the types located in the provided file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> [TypeInfo] {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result = [TypeInfo]()+    let visitor = TypeInfoSyntaxVisitor() { typeInfo in

Oh duh! 🤦

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick on 8/13/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick+import Foundation++@testable import SwiftInspectorCore++final class TypesCommandSpec: QuickSpec {+  override func spec() {+    var fileURL: URL!++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }+++    describe("run") {+      context("with no arguments") {+        it("fails") {+          let result = try? TestTask.run(withArguments: ["types"])+          expect(result?.didFail) == true+        }+      }++      context("when path is invalid") {+        it("fails when empty") {+          let result = try? TestTypesCommandTask.run(path: "")+          expect(result?.didFail) == true+        }++        it("fails when it doesn't exist") {+          let result = try? TestTypesCommandTask.run(path: "/fake/path")+          expect(result?.didFail) == true+        }+      }++      context("when path is valid") {
      context("when path is valid file") {
thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick on 8/13/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick+import Foundation++@testable import SwiftInspectorCore++final class TypesCommandSpec: QuickSpec {+  override func spec() {+    var fileURL: URL!++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }+++    describe("run") {+      context("with no arguments") {+        it("fails") {+          let result = try? TestTask.run(withArguments: ["types"])+          expect(result?.didFail) == true+        }+      }++      context("when path is invalid") {+        it("fails when empty") {+          let result = try? TestTypesCommandTask.run(path: "")+          expect(result?.didFail) == true+        }++        it("fails when it doesn't exist") {+          let result = try? TestTypesCommandTask.run(path: "/fake/path")+          expect(result?.didFail) == true+        }+      }++      context("when path is valid") {+        it("succeeds") {+          fileURL = try? Temporary.makeFile(content: "struct Foo { }")+          let path = fileURL?.path ?? ""+          let result = try? TestTypesCommandTask.run(path: path)+          expect(result?.didSucceed) == true+        }++        it("runs and outputs the result without comment") {+          fileURL = try? Temporary.makeFile(content: """+                                                   // This is a comment+                                                   struct Foo { }+                                                   """)+          let path = fileURL?.path ?? ""+          let result = try? TestTypesCommandTask.run(path: path)+          expect(result?.outputMessage).to(contain("Foo,struct"))

We can refactor these 2 tests to put the common file in a beforeEach so this looks like:

var result: TaskStatus!

beforeEach {
  fileURL = try? Temporary.makeFile(content: "struct Foo { }")
  let path = fileURL?.path ?? ""
  result = try? TestTypesCommandTask.run(path: path)
}

it("succeeds") {
  expect(result?.didSucceed) == true
}

it("outputs the type name and Type") {
  expect(result?.outputMessage).to(contain("Foo,struct"))
}
thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

 final class TypesCommand: ParsableCommand {     return "\(info.name),\(info.type),\(comment)"   } }++private var commentFlagHelp = ArgumentHelp(+  "The granularity of the output",
  "Whether to include any preceding comments on top of the type",
thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

 final class TypesCommandSpec: QuickSpec {                                                    struct Foo { }                                                    """)           let path = fileURL?.path ?? ""-          let result = try? TestTask.run(withArguments: ["types", "--path", path])-          expect(result!.outputMessage).to(contain("Foo,struct,// This is a comment"))+          let result = try? TestTypesCommandTask.run(path: path, arguments: ["--enable-include-comments"])+          expect(result?.outputMessage).to(contain("Foo,struct,// This is a comment"))+        }+      }++      context("when path is a folder") {+        var folderURL: URL!+        beforeEach {+          folderURL = try! Temporary.makeFolder()+        }+        afterEach {+          try! Temporary.removeItem(at: folderURL)+        }++        it("succeeds") {+          let result = try? TestTypesCommandTask.run(path: folderURL.path)++          expect(result?.didSucceed) == true+        }++        it("outputs the correct types") {+          let _ = try? Temporary.makeFile(+            content: """+                     class Foo { }+                     """,+            atPath: folderURL.path)++          let _ = try? Temporary.makeFile(+          content: """+                   class Bar { }+                   """,+          atPath: folderURL.path)++          let result = try? TestTypesCommandTask.run(path: folderURL.path)+          let outputMessageLines = result?.outputMessage?.split { $0.isNewline }+          expect(outputMessageLines).to(contain(["Foo,class", "Bar,class"]))         }       }      }   } }++private struct TestTypesCommandTask {

👍

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick on 8/13/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick+import Foundation++@testable import SwiftInspectorCore++final class TypesCommandSpec: QuickSpec {+  override func spec() {+    var fileURL: URL!++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }+++    describe("run") {+      context("with no arguments") {+        it("fails") {+          let result = try? TestTask.run(withArguments: ["types"])+          expect(result?.didFail) == true+        }+      }++      context("when path is invalid") {+        it("fails when empty") {+          let result = try? TestTask.run(withArguments: ["types", "--path", ""])+          expect(result?.didFail) == true+        }++        it("fails when it doesn't exist") {+          let result = try? TestTask.run(withArguments: ["types", "--path", "/fake/path"])+          expect(result?.didFail) == true+        }+      }++      context("when path is valid") {+        it("runs and outputs the result (without comment)") {+          fileURL = try? Temporary.makeFile(content: "struct Foo { }")+          let path = fileURL?.path ?? ""+          let result = try? TestTask.run(withArguments: ["types", "--path", path])+          expect(result!.outputMessage).to(contain("Foo,struct"))+        }++        it("runs and outputs the result (with comment)") {+          fileURL = try? Temporary.makeFile(content: """+                                                   // This is a comment+                                                   struct Foo { }+                                                   """)+          let path = fileURL?.path ?? ""+          let result = try? TestTask.run(withArguments: ["types", "--path", path])+          expect(result!.outputMessage).to(contain("Foo,struct,// This is a comment"))

I'm curious about why we're adding comments too to the output? Curious about the use case.

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick on 8/12/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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++public final class TypesAnalyzer: Analyzer {++  /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from+  public init(cachedSyntaxTree: CachedSyntaxTree = .init()) {+    self.cachedSyntaxTree = cachedSyntaxTree+  }++  /// Analyzes the types located in the provided file+  /// - Parameter fileURL: The fileURL where the Swift file is located+  public func analyze(fileURL: URL) throws -> [TypeInfo] {+    let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL)+    var result = [TypeInfo]()+    let visitor = TypeInfoSyntaxVisitor() { typeInfo in
    let visitor = TypeInfoSyntaxVisitor() { [unowned self] typeInfo in
thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick 8/13/20.+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick++@testable import SwiftInspectorCore++final class TypesAnalyzerSpec: QuickSpec {++  override func spec() {+    var fileURL: URL!+    var sut: TypesAnalyzer!++    beforeEach {+      sut = TypesAnalyzer()+    }++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }++    describe("analyze(fileURL:)") {+      context("there are no types present") {+        beforeEach {+          let content =+          """+          import Foundation+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns empty array") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).to(beEmpty())+        }+      }++      context("struct is present") {+        beforeEach {+          let content =+          """+          struct Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the struct") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .struct+        }+      }++      context("enum is present") {+        beforeEach {+          let content =+          """+          enum Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the enum") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .enum+        }+      }++      context("class is present") {+        beforeEach {+          let content =+          """+          class Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the class") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .class+        }+      }++      context("protocol is present") {+        beforeEach {+          let content =+          """+          protocol Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the protocol") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .protocol+        }+      }++      context("struct is present with comment") {+        beforeEach {+          let content =+          """+          // This is a comment+          struct Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the struct including the comment") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .struct+          expect(result!.first!.comment) == "// This is a comment"+        }+      }++      context("enum is present with comment") {+        beforeEach {+          let content =+          """+          // This is a comment+          enum Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the enum including the comment") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .enum+          expect(result!.first!.comment) == "// This is a comment"+        }+      }++      context("class is present with comment") {+        beforeEach {+          let content =+          """+          // This is a comment+          class Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the class including the comment") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .class+          expect(result!.first!.comment) == "// This is a comment"+        }+      }++      context("protocol is present with comment") {+        beforeEach {+          let content =+          """+          // This is a comment+          protocol Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the protocol including the comment") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .protocol+          expect(result!.first!.comment) == "// This is a comment"+        }+      }++      context("multiple types present") {+        beforeEach {+          let content =+          """+          // This is a comment+          protocol Foo { }++          // This is a different comment+          private final class Bar: Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for both types including the comments") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .protocol+          expect(result!.first!.comment) == "// This is a comment"++          expect(result![1].name) == "Bar"+          expect(result![1].type) == .class+          expect(result![1].comment) == "// This is a different comment"+        }+      }

Let's remove the force unwrapping from these tests, we can just get the same behavior (without crashing) by using optional chaining instead.

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick 8/13/20.+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick++@testable import SwiftInspectorCore++final class TypesAnalyzerSpec: QuickSpec {++  override func spec() {+    var fileURL: URL!+    var sut: TypesAnalyzer!++    beforeEach {+      sut = TypesAnalyzer()+    }++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }++    describe("analyze(fileURL:)") {+      context("there are no types present") {+        beforeEach {+          let content =+          """+          import Foundation+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns empty array") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).to(beEmpty())+        }+      }++      context("struct is present") {+        beforeEach {+          let content =+          """+          struct Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the struct") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .struct+        }+      }++      context("enum is present") {+        beforeEach {+          let content =+          """+          enum Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the enum") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .enum+        }+      }++      context("class is present") {+        beforeEach {+          let content =+          """+          class Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the class") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .class+        }

One of the cool things about Quick is that these no longer need to be one test. You can split them extremely easily into one assertion per test


      context("class is present") {
        var result: TypeInfo?

        beforeEach {
          let content =
          """
          class Foo { }
          """
          fileURL = try? Temporary.makeFile(content: content)
          result = try? sut.analyze(fileURL: fileURL)
        }

        it("returns the type name") {
          expect(result?.first?.name) == "Foo"
        }

        it("returns the type's Type") {
          expect(result?.first?.type) == .class
        }

        it("returns the type comments as empty") {
          expect(result?.first?.comment?.isEmpty) == true
        }
thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick 8/13/20.+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick++@testable import SwiftInspectorCore++final class TypesAnalyzerSpec: QuickSpec {++  override func spec() {+    var fileURL: URL!+    var sut: TypesAnalyzer!++    beforeEach {+      sut = TypesAnalyzer()+    }++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }++    describe("analyze(fileURL:)") {+      context("there are no types present") {+        beforeEach {+          let content =+          """+          import Foundation+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns empty array") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).to(beEmpty())+        }+      }++      context("struct is present") {+        beforeEach {+          let content =+          """+          struct Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the struct") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .struct+        }+      }++      context("enum is present") {+        beforeEach {+          let content =+          """+          enum Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the enum") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .enum+        }+      }++      context("class is present") {+        beforeEach {+          let content =+          """+          class Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the class") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .class+        }+      }++      context("protocol is present") {+        beforeEach {+          let content =+          """+          protocol Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the protocol") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .protocol+        }+      }++      context("struct is present with comment") {+        beforeEach {+          let content =+          """+          // This is a comment+          struct Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the struct including the comment") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .struct+          expect(result!.first!.comment) == "// This is a comment"+        }+      }++      context("enum is present with comment") {+        beforeEach {+          let content =+          """+          // This is a comment+          enum Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the enum including the comment") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .enum+          expect(result!.first!.comment) == "// This is a comment"+        }+      }++      context("class is present with comment") {+        beforeEach {+          let content =+          """+          // This is a comment+          class Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the class including the comment") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .class+          expect(result!.first!.comment) == "// This is a comment"+        }+      }++      context("protocol is present with comment") {+        beforeEach {+          let content =+          """+          // This is a comment

Should we validate other types of comments? Like /// and /**

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick 8/13/20.+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick++@testable import SwiftInspectorCore++final class TypesAnalyzerSpec: QuickSpec {++  override func spec() {+    var fileURL: URL!+    var sut: TypesAnalyzer!++    beforeEach {+      sut = TypesAnalyzer()+    }++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }++    describe("analyze(fileURL:)") {+      context("there are no types present") {+        beforeEach {+          let content =+          """+          import Foundation+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns empty array") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).to(beEmpty())+        }+      }++      context("struct is present") {+        beforeEach {+          let content =+          """+          struct Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the struct") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .struct
          expect(result?.first?.type) == .struct
thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick 8/13/20.+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick++@testable import SwiftInspectorCore++final class TypesAnalyzerSpec: QuickSpec {++  override func spec() {+    var fileURL: URL!+    var sut: TypesAnalyzer!++    beforeEach {+      sut = TypesAnalyzer()+    }++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }++    describe("analyze(fileURL:)") {+      context("there are no types present") {+        beforeEach {+          let content =+          """+          import Foundation+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns empty array") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).to(beEmpty())+        }+      }++      context("struct is present") {+        beforeEach {+          let content =+          """+          struct Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the struct") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .struct+        }+      }++      context("enum is present") {+        beforeEach {+          let content =+          """+          enum Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the enum") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .enum
          expect(result?.first?.type) == .enum
thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick on 8/13/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick+import Foundation++@testable import SwiftInspectorCore++final class TypesCommandSpec: QuickSpec {+  override func spec() {+    var fileURL: URL!++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }+++    describe("run") {+      context("with no arguments") {+        it("fails") {+          let result = try? TestTask.run(withArguments: ["types"])+          expect(result?.didFail) == true+        }+      }++      context("when path is invalid") {+        it("fails when empty") {+          let result = try? TestTask.run(withArguments: ["types", "--path", ""])+          expect(result?.didFail) == true+        }++        it("fails when it doesn't exist") {+          let result = try? TestTask.run(withArguments: ["types", "--path", "/fake/path"])+          expect(result?.didFail) == true+        }+      }++      context("when path is valid") {+        it("runs and outputs the result (without comment)") {+          fileURL = try? Temporary.makeFile(content: "struct Foo { }")+          let path = fileURL?.path ?? ""+          let result = try? TestTask.run(withArguments: ["types", "--path", path])+          expect(result!.outputMessage).to(contain("Foo,struct"))
          expect(result?.outputMessage).to(contain("Foo,struct"))
thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick 8/13/20.+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick++@testable import SwiftInspectorCore++final class TypesAnalyzerSpec: QuickSpec {++  override func spec() {+    var fileURL: URL!+    var sut: TypesAnalyzer!++    beforeEach {+      sut = TypesAnalyzer()+    }++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }++    describe("analyze(fileURL:)") {+      context("there are no types present") {+        beforeEach {+          let content =+          """+          import Foundation+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns empty array") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).to(beEmpty())+        }+      }++      context("struct is present") {+        beforeEach {+          let content =+          """+          struct Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the struct") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"+          expect(result!.first!.type) == .struct+        }+      }++      context("enum is present") {+        beforeEach {+          let content =+          """+          enum Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the enum") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"
          expect(result?.first?.name) == "Foo"
thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick on 8/12/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 ArgumentParser+import Foundation+import SwiftInspectorCore++final class TypesCommand: ParsableCommand {+  static var configuration = CommandConfiguration(+    commandName: "types",+    abstract: "Finds information about types in a file"+  )++  @Option(help: "The absolute path to the file to inspect")+  var path: String++  /// Runs the command+  func run() throws {+    let cachedSyntaxTree = CachedSyntaxTree()+    let analyzer = TypesAnalyzer(cachedSyntaxTree: cachedSyntaxTree)+    let fileURL = URL(fileURLWithPath: path)+    let outputArray = try FileManager.default.swiftFiles(at: fileURL)

Seems like you set up this to work with folders 😬 this is gonna make https://github.com/fdiaz/SwiftInspector/issues/42 a lot easier to adopt. I think you can actually just change the documentation on path on the command and add a couple of tests and we should be good to go to support both a single swift file or a folder.

thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick 8/13/20.+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick++@testable import SwiftInspectorCore++final class TypesAnalyzerSpec: QuickSpec {++  override func spec() {+    var fileURL: URL!+    var sut: TypesAnalyzer!++    beforeEach {+      sut = TypesAnalyzer()+    }++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }++    describe("analyze(fileURL:)") {+      context("there are no types present") {+        beforeEach {+          let content =+          """+          import Foundation+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns empty array") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).to(beEmpty())+        }+      }++      context("struct is present") {+        beforeEach {+          let content =+          """+          struct Foo { }+          """+          fileURL = try? Temporary.makeFile(content: content)+        }++        it("returns the type information for the struct") {+          let result = try? sut.analyze(fileURL: fileURL)+          expect(result).notTo(beEmpty())+          expect(result!.first!.name) == "Foo"
          expect(result?.first?.name) == "Foo"
thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick on 8/13/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick+import Foundation++@testable import SwiftInspectorCore++final class TypesCommandSpec: QuickSpec {+  override func spec() {+    var fileURL: URL!++    afterEach {+      guard let fileURL = fileURL else {+        return+      }+      try? Temporary.removeItem(at: fileURL)+    }+++    describe("run") {+      context("with no arguments") {+        it("fails") {+          let result = try? TestTask.run(withArguments: ["types"])+          expect(result?.didFail) == true+        }+      }++      context("when path is invalid") {+        it("fails when empty") {+          let result = try? TestTask.run(withArguments: ["types", "--path", ""])+          expect(result?.didFail) == true+        }++        it("fails when it doesn't exist") {+          let result = try? TestTask.run(withArguments: ["types", "--path", "/fake/path"])+          expect(result?.didFail) == true+        }+      }++      context("when path is valid") {+        it("runs and outputs the result (without comment)") {+          fileURL = try? Temporary.makeFile(content: "struct Foo { }")+          let path = fileURL?.path ?? ""+          let result = try? TestTask.run(withArguments: ["types", "--path", path])+          expect(result!.outputMessage).to(contain("Foo,struct"))+        }++        it("runs and outputs the result (with comment)") {+          fileURL = try? Temporary.makeFile(content: """+                                                   // This is a comment+                                                   struct Foo { }+                                                   """)+          let path = fileURL?.path ?? ""+          let result = try? TestTask.run(withArguments: ["types", "--path", path])+          expect(result!.outputMessage).to(contain("Foo,struct,// This is a comment"))
          expect(result?.outputMessage).to(contain("Foo,struct,// This is a comment"))
thedrick

comment created time in a month

Pull request review commentfdiaz/SwiftInspector

Add types analyzer

+// Created by Tyler Hedrick on 8/13/20.+//+// Copyright (c) 2020 Tyler Hedrick+//+// Distributed under the MIT License+//+// 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 Nimble+import Quick+import Foundation++@testable import SwiftInspectorCore++final class TypesCommandSpec: QuickSpec {+  override func spec() {+    describe("run") {++      context("with no arguments") {+        it("fails") {+          let result = try? TestTask.run(withArguments: ["types"])+          expect(result?.didFail) == true+        }+      }++      context("when path is invalid") {+        it("fails when empty") {+          let result = try? TestTask.run(withArguments: ["types", "--path", ""])+          expect(result?.didFail) == true+        }++        it("fails when it doesn't exist") {+          let result = try? TestTask.run(withArguments: ["types", "--path", "/fake/path"])+          expect(result?.didFail) == true+        }

Let's add a success case here

thedrick

comment created time in a month

issue openedfdiaz/SwiftInspector

Make types command work with folders

This PR https://github.com/fdiaz/SwiftInspector/pull/41 introduced the types command, but it only works today with files.

Let's extend it to work with folders too.

created time in a month

issue openedfdiaz/SwiftInspector

Document what our stance on the format of the output is

When contributing it's not clear what the output of a new command should look like. We should write a document that states our reasoning behind output of the tool

created time in a month

startedtuist/tuist

started time in 2 months

issue closedairbnb/swift

Place function/type attributes on the line above the declaration

This rule is missing autocorrect. Following this PR.


  • <a id='attributes-on-prev-line'></a>(<a href='#attributes-on-prev-line'>link</a>) Place function/type attributes on the line above the declaration.

    <details>

    // WRONG
    @objc class Spaceship: NSObject {
    
      @discardableResult func fly() -> Bool {
      }
    }
    
    // RIGHT
    
    @objc
    class Spaceship: NSObject {
    
      @discardableResult
      func fly() -> Bool {
      }
    }
    

    </details>

closed time in 2 months

fdiaz

issue commentairbnb/swift

Place function/type attributes on the line above the declaration

Added in https://github.com/airbnb/swift/pull/101

fdiaz

comment created time in 2 months

issue closedairbnb/swift

Multi-line arrays should have each bracket on a separate line

This rule is missing autocorrect. Following this PR.


  • <a id='multi-line-array'></a>(<a href='#multi-line-array'>link</a>) Multi-line arrays should have each bracket on a separate line. Put the opening and closing brackets on separate lines from any of the elements of the array. Also add a trailing comma on the last element.

    <details>

    // WRONG
    let rowContent = [listingUrgencyDatesRowContent(),
                      listingUrgencyBookedRowContent(),
                      listingUrgencyBookedShortRowContent()]
    
    let rowContent = [
      listingUrgencyDatesRowContent(),
      listingUrgencyBookedRowContent(),
      listingUrgencyBookedShortRowContent()
    ]
    
    // RIGHT
    let rowContent = [
      listingUrgencyDatesRowContent(),
      listingUrgencyBookedRowContent(),
      listingUrgencyBookedShortRowContent(),
    ]
    

    </details>

closed time in 2 months

fdiaz

issue commentairbnb/swift

Multi-line arrays should have each bracket on a separate line

Added in https://github.com/airbnb/swift/pull/101

fdiaz

comment created time in 2 months

issue closedairbnb/swift

Long function invocations should also break on each argument

This rule is missing autocorrect. Following this PR.


  • <a id='long-function-invocation'></a>(<a href='#long-function-invocation'>link</a>) Long function invocations should also break on each argument. Put the closing parenthesis on the last line of the invocation. SwiftLint: multiline_arguments SwiftLint: vertical_parameter_alignment_on_call

    <details>

    universe.generateStars(
      at: location,
      count: 5,
      color: starColor,
      withAverageDistance: 4)
    
    universe.generate(
      5,
      .stars,
      at: location)
    

    </details>

closed time in 2 months

fdiaz

issue commentairbnb/swift

Long function invocations should also break on each argument

Added in https://github.com/airbnb/swift/pull/101

fdiaz

comment created time in 2 months

issue closedairbnb/swift

Separate long function declarations with line breaks before each argument label and before the return signature.

This rule is missing autocorrect. Following this PR.


  • <a id='long-function-declaration'></a>(<a href='#long-function-declaration'>link</a>) Separate long function declarations with line breaks before each argument label and before the return signature. Put the open curly brace on the next line so the first executable line doesn't look like it's another parameter. SwiftLint: multiline_parameters SwiftLint: vertical_parameter_alignment_on_call

    <details>

    class Universe {
    
      // WRONG
      func generateStars(at location: Point, count: Int, color: StarColor, withAverageDistance averageDistance: Float) -> String {
        // This is too long and will probably auto-wrap in a weird way
      }
    
      // WRONG
      func generateStars(at location: Point,
                         count: Int,
                         color: StarColor,
                         withAverageDistance averageDistance: Float) -> String
      {
        // Xcode indents all the arguments
      }
    
      // WRONG
      func generateStars(
        at location: Point,
        count: Int,
        color: StarColor,
        withAverageDistance averageDistance: Float) -> String {
        populateUniverse() // this line blends in with the argument list
      }
    
      // WRONG
      func generateStars(
        at location: Point,
        count: Int,
        color: StarColor,
        withAverageDistance averageDistance: Float) throws
        -> String {
        populateUniverse() // this line blends in with the argument list
      }
    
      // RIGHT
      func generateStars(
        at location: Point,
        count: Int,
        color: StarColor,
        withAverageDistance averageDistance: Float) 
        -> String
      {
        populateUniverse()
      }
    
      // RIGHT
      func generateStars(
        at location: Point,
        count: Int,
        color: StarColor,
        withAverageDistance averageDistance: Float) 
        throws -> String
      {
        populateUniverse()
      }
    }
    

    </details>

closed time in 2 months

fdiaz

issue commentairbnb/swift

Separate long function declarations with line breaks before each argument label and before the return signature.

Added in https://github.com/airbnb/swift/pull/101

fdiaz

comment created time in 2 months

Pull request review commentairbnb/swift

Reintroduce old wrapping rules

 _You can enable the following settings in Xcode by running [this script](resourc    </details> +* <a id='long-function-declaration'></a>(<a href='#long-function-declaration'>link</a>) **Separate [long](https://github.com/airbnb/swift#column-width) function declarations with line breaks before each argument label and before the return signature.** Put the open curly brace on the next line so the first executable line doesn't look like it's another parameter. [![SwiftFormat: wrapArguments](https://img.shields.io/badge/SwiftFormat-wrapArguments-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#wrapArguments) [![SwiftFormat: wrapMultilineStatementBraces](https://img.shields.io/badge/SwiftFormat-wrapArguments-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#wrapArguments)

Let's move this down to the Functions section

calda

comment created time in 2 months

Pull request review commentairbnb/swift

Reintroduce old wrapping rules

 _You can enable the following settings in Xcode by running [this script](resourc    </details> +* <a id='long-function-declaration'></a>(<a href='#long-function-declaration'>link</a>) **Separate [long](https://github.com/airbnb/swift#column-width) function declarations with line breaks before each argument label and before the return signature.** Put the open curly brace on the next line so the first executable line doesn't look like it's another parameter. [![SwiftFormat: wrapArguments](https://img.shields.io/badge/SwiftFormat-wrapArguments-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#wrapArguments) [![SwiftFormat: wrapMultilineStatementBraces](https://img.shields.io/badge/SwiftFormat-wrapArguments-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#wrapArguments)

The second badge name needs to be updated:

* <a id='long-function-declaration'></a>(<a href='#long-function-declaration'>link</a>) **Separate [long](https://github.com/airbnb/swift#column-width) function declarations with line breaks before each argument label and before the return signature.** Put the open curly brace on the next line so the first executable line doesn't look like it's another parameter. [![SwiftFormat: wrapArguments](https://img.shields.io/badge/SwiftFormat-wrapArguments-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#wrapArguments) [![SwiftFormat: wrapMultilineStatementBraces](https://img.shields.io/badge/SwiftFormat-wrapMultilineStatementBraces-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#wrapArguments)
calda

comment created time in 2 months

Pull request review commentairbnb/swift

Reintroduce old wrapping rules

 _You can enable the following settings in Xcode by running [this script](resourc   ```    </details>+  +* <a id='attributes-on-prev-line'></a>(<a href='#attributes-on-prev-line'>link</a>) **Place function/type attributes on the line above the declaration**. [![SwiftFormat: wrapAttributes](https://img.shields.io/badge/SwiftFormat-wrapAttributes-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#wrapAttributes)

Let's move this down to Functions

calda

comment created time in 2 months

Pull request review commentairbnb/swift

Reintroduce old wrapping rules

 _You can enable the following settings in Xcode by running [this script](resourc    </details> +* <a id='long-function-declaration'></a>(<a href='#long-function-declaration'>link</a>) **Separate [long](https://github.com/airbnb/swift#column-width) function declarations with line breaks before each argument label and before the return signature.** Put the open curly brace on the next line so the first executable line doesn't look like it's another parameter. [![SwiftFormat: wrapArguments](https://img.shields.io/badge/SwiftFormat-wrapArguments-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#wrapArguments) [![SwiftFormat: wrapMultilineStatementBraces](https://img.shields.io/badge/SwiftFormat-wrapArguments-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#wrapArguments)++  <details>++  ```swift+  class Universe {++    // WRONG+    func generateStars(at location: Point, count: Int, color: StarColor, withAverageDistance averageDistance: Float) -> String {+      // This is too long and will probably auto-wrap in a weird way+    }++    // WRONG+    func generateStars(at location: Point,+                       count: Int,+                       color: StarColor,+                       withAverageDistance averageDistance: Float) -> String+    {+      // Xcode indents all the arguments+    }++    // WRONG+    func generateStars(+      at location: Point,+      count: Int,+      color: StarColor,+      withAverageDistance averageDistance: Float) -> String {+      populateUniverse() // this line blends in with the argument list+    }++    // WRONG+    func generateStars(+      at location: Point,+      count: Int,+      color: StarColor,+      withAverageDistance averageDistance: Float) throws+      -> String {+      populateUniverse() // this line blends in with the argument list+    }++    // RIGHT+    func generateStars(+      at location: Point,+      count: Int,+      color: StarColor,+      withAverageDistance averageDistance: Float) +      -> String+    {+      populateUniverse()+    }++    // RIGHT+    func generateStars(+      at location: Point,+      count: Int,+      color: StarColor,+      withAverageDistance averageDistance: Float) +      throws -> String+    {+      populateUniverse()+    }+  }+  ```++  </details>++* <a id='long-function-invocation'></a>(<a href='#long-function-invocation'>link</a>) **[Long](https://github.com/airbnb/swift#column-width) function invocations should also break on each argument.** Put the closing parenthesis on the last line of the invocation. [![SwiftFormat: wrapArguments](https://img.shields.io/badge/SwiftFormat-wrapArguments-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#wrapArguments)++  <details>++  ```swift+  universe.generateStars(+    at: location,+    count: 5,+    color: starColor,+    withAverageDistance: 4)++  universe.generate(+    5,+    .stars,+    at: location)+  ```++  </details>+  +  * <a id='multi-line-array'></a>(<a href='#multi-line-array'>link</a>) **Multi-line arrays should have each bracket on a separate line.** Put the opening and closing brackets on separate lines from any of the elements of the array. Also add a trailing comma on the last element. [![SwiftFormat: wrapArguments](https://img.shields.io/badge/SwiftFormat-wrapArguments-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#wrapArguments)
* <a id='multi-line-array'></a>(<a href='#multi-line-array'>link</a>) **Multi-line arrays should have each bracket on a separate line.** Put the opening and closing brackets on separate lines from any of the elements of the array. Also add a trailing comma on the last element. [![SwiftFormat: wrapArguments](https://img.shields.io/badge/SwiftFormat-wrapArguments-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#wrapArguments)
calda

comment created time in 2 months

issue commentairbnb/ResilientDecoding

Support passthrough conformances for encoding and equatable

This is very similar to https://github.com/airbnb/ResilientDecoding/pull/38

cc @GeorgeLyon

matthewcheok

comment created time in 2 months

delete branch fdiaz/SwiftInspector

delete branch : master

delete time in 2 months

push eventfdiaz/SwiftInspector

Francisco Diaz

commit sha 56d6325e17d9b07ce0450bd05a5c7f13ad3f5b42

Update README (#39)

view details

push time in 2 months

delete branch fdiaz/SwiftInspector

delete branch : default-branch

delete time in 2 months

PR merged fdiaz/SwiftInspector

Update README to reflect default branch

Following https://github.com/fdiaz/SwiftInspector/issues/38 we're renaming master -> main.

+4 -0

1 comment

1 changed file

fdiaz

pr closed time in 2 months

push eventfdiaz/SwiftInspector

Francisco Diaz

commit sha ff62d994e4cf89f856336fcd684230c0272b6cf1

Update README

view details

push time in 2 months

more