Open22

swift-format でキチンと書いているつもりの Doc Comment に「パラメータがマッチしてない」警告がでる謎を追う

griffin_stewiegriffin_stewie

当初、以下の tweet を発端に Twitter の Thread で色々書いていた。調べていく内に tweet におさめられなく鳴ってきたと感じたので Zenn の Scrap 機能を使ってみることにした。

最初のこの投稿は Tweet のリンクを貼るが、それ以降しばらくは tweet の内容をコピペしてここに転載するようにしようと思う。

https://twitter.com/griffin_stewie/status/1479621896732049408

griffin_stewiegriffin_stewie

Apple のバグレポには一応登録済み。
教えてもらった JIRA にはもうちょっと調べてからちゃんと書こうかなと思っているところ。

griffin_stewiegriffin_stewie

https://twitter.com/hokuron/status/1479623637049425927
上記のようにテストコード見てみると良いよとアドバイスをいただいた。感謝。
実際に見てみたら、自分のコメントの書き方で問題なさそうだと思った。

特に不安だったのが、ラベルと仮引数の両方を定義しているような関数。以下のようなやつ。

func open(with url: URL) { }

実際のところ、テストコードをみると パラメータ名としては、仮引数の方をコメントとして記載するのが正しい模様。

griffin_stewiegriffin_stewie

もうすこし分かった。1行目の Function の Description 文の中にピリオドがあれば、Parameters の中でピリオドがあってもなくても問題なく動くっぽい。3番目の ok2 って関数のDescriptionにはピリオドがあるのでParametersのピリオドの位置は失敗するコメントと同じ位置だけどリントに引っかからない。

https://twitter.com/griffin_stewie/status/1479814218304360449

griffin_stewiegriffin_stewie

この時点 では、Parameters の項目が複数あるとき、最後の項目以外にピリオドを使うとおかしくなると思っていた。

しかし、もう少し調査が進むと Doc Comment の最初の行にピリオドがあれば Parameters の項目にどうピリオドが含まれていても問題がないことが分かった。

Doc Comment 全体(最初の行から、各種パラメータや戻り値の説明なども含めたコメント全体)から、swift-format が構文解析した時に

  • ピリオドを特別視してパースしている
  • 最初の行からそのようにパースしているので、後続がおかしくなる

という仮説がたった。

griffin_stewiegriffin_stewie

じゃあ、構文解析部分のコードをデバッグしてブレイクポイント貼ったりして調べようと思った。だが、そもそもうまく動かない問題が発生。

apple/swift-format は、ブランチ・タグ毎に対応している Swift のバージョンが違うらしい。僕の環境は Xcode 13.2.1 なので swift-5.5-branch タグを使うべきだという理解。

このコードの Package.swift を Xcode 13.2.1 で開いて swift-format-Package schemeに対して ⌘+U でユニットテストを実行しても、
「バンドル“SwiftFormatWhitespaceLinterTests”を読み込めませんでした。 バンドルを再インストールしてください。」
というエラーと
Library not loaded: @rpath/lib_InternalSwiftSyntaxParser.dylib とかが出てしまい、実行できなかった。

コマンドラインから swift test を実行したらちゃんと動く。

なのでしばらくは、コマンドラインからテストを実行していた。

griffin_stewiegriffin_stewie

コマンドラインからテストを実行する場合、任意のテストだけを実行したい場合は --filter オプションを利用すれば良さそうだった。

help を読むと --filter オプションの説明には

--filter <filter>       Run test cases matching regular expression, Format: <test-target>.<test-case> or <test-target>.<test-case>/<test> 

と書かれていて、正規表現で指定できたりするっぽい。

僕は細かく指定するのが面倒だったので、テストターゲットだけを指定して以下のようなコマンドで試していた。

swift test --filter ValidateDocumentationCommentsTests
griffin_stewiegriffin_stewie

Xcode で任意の箇所にブレイクポイントを貼って試す方法として、デバッグビルドしたバイナリを ⌘+R で実行して自動的に LLDB を起動する方法が1番一般的だし、デフォルトではそのような設定になっている。

でも、ビルド済みのデバッグビルドなどを実行時に LLDB をアタッチしてくれる設定もある。これは Sketch Plugin など Objective-C で書いたプラグインをデバッグするときによく使ったテクニックだったのでそれを試したみた。

結果は結局 Library not loaded: @rpath/lib_InternalSwiftSyntaxParser.dylib が発生してダメ。

griffin_stewiegriffin_stewie

swift-format の Package.swift をおもちさんの対処方法を真似て以下のようにしたら、素直に Xcode からテストも走るし、swift-format コマンドも実行できるようになった。

// swift-tools-version:5.1
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import PackageDescription
import Foundation

let rpath = "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx"

let package = Package(
  name: "swift-format",
  products: [
    .executable(name: "swift-format", targets: ["swift-format"]),
    .library(name: "SwiftFormat", targets: ["SwiftFormat", "SwiftFormatConfiguration"]),
    .library(name: "SwiftFormatConfiguration", targets: ["SwiftFormatConfiguration"]),
  ],
  dependencies: [
  ],
  targets: [
    .target(
      name: "SwiftFormat",
      dependencies: [
        "SwiftFormatConfiguration",
        "SwiftFormatCore",
        "SwiftFormatPrettyPrint",
        "SwiftFormatRules",
        "SwiftFormatWhitespaceLinter",
        "SwiftSyntax",
      ]
    ),
    .target(name: "SwiftFormatConfiguration"),
    .target(name: "SwiftFormatCore", dependencies: ["SwiftFormatConfiguration", "SwiftSyntax"]),
    .target(
      name: "SwiftFormatRules",
      dependencies: ["SwiftFormatCore", "SwiftFormatConfiguration"]
    ),
    .target(
      name: "SwiftFormatPrettyPrint",
      dependencies: ["SwiftFormatCore", "SwiftFormatConfiguration"]
    ),
    .target(
      name: "SwiftFormatTestSupport",
      dependencies: [
        "SwiftFormatCore",
        "SwiftFormatRules",
        "SwiftFormatConfiguration",
      ]
    ),
    .target(
      name: "SwiftFormatWhitespaceLinter",
      dependencies: [
        "SwiftFormatCore",
        "SwiftSyntax",
      ]
    ),
    .target(
      name: "generate-pipeline",
      dependencies: [
        "SwiftFormatCore",
        "SwiftFormatRules",
        "SwiftSyntax",
      ]
    ),
    .target(
      name: "swift-format",
      dependencies: [
        "ArgumentParser",
        "SwiftFormat",
        "SwiftFormatConfiguration",
        "SwiftFormatCore",
        "SwiftSyntax",
      ],
      linkerSettings: [
          .unsafeFlags(["-rpath", rpath])
      ]
    ),
    .testTarget(
      name: "SwiftFormatTests",
      dependencies: [
        "SwiftFormat",
      ],
      linkerSettings: [
          .unsafeFlags(["-rpath", rpath])
      ]
    ),
    .testTarget(
      name: "SwiftFormatConfigurationTests",
      dependencies: ["SwiftFormatConfiguration"],
      linkerSettings: [
          .unsafeFlags(["-rpath", rpath])
      ]
    ),
    .testTarget(
      name: "SwiftFormatRulesTests",
      dependencies: [
        "SwiftFormatConfiguration",
        "SwiftFormatCore",
        "SwiftFormatPrettyPrint",
        "SwiftFormatRules",
        "SwiftFormatTestSupport",
        "SwiftSyntax",
      ],
      linkerSettings: [
          .unsafeFlags(["-rpath", rpath])
      ]
    ),
    .testTarget(
      name: "SwiftFormatCoreTests",
      dependencies: [
        "SwiftFormatConfiguration",
        "SwiftFormatCore",
        "SwiftSyntax",
      ],
      linkerSettings: [
          .unsafeFlags(["-rpath", rpath])
      ]
    ),
    .testTarget(
      name: "SwiftFormatPerformanceTests",
      dependencies: [
        "SwiftFormatTestSupport",
        "SwiftFormatWhitespaceLinter",
        "SwiftSyntax",
      ],
      linkerSettings: [
          .unsafeFlags(["-rpath", rpath])
      ]
    ),
    .testTarget(
      name: "SwiftFormatPrettyPrintTests",
      dependencies: [
        "SwiftFormatConfiguration",
        "SwiftFormatCore",
        "SwiftFormatPrettyPrint",
        "SwiftFormatRules",
        "SwiftFormatTestSupport",
        "SwiftSyntax",
      ],
      linkerSettings: [
          .unsafeFlags(["-rpath", rpath])
      ]
    ),
    .testTarget(
      name: "SwiftFormatWhitespaceLinterTests",
      dependencies: [
        "SwiftFormatConfiguration",
        "SwiftFormatCore",
        "SwiftFormatTestSupport",
        "SwiftFormatWhitespaceLinter",
        "SwiftSyntax",
      ],
      linkerSettings: [
          .unsafeFlags(["-rpath", rpath])
      ]
    ),
  ]
)


if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
  // Building standalone.
  package.dependencies += [
    .package(url: "https://github.com/apple/swift-syntax", .upToNextMinor(from: "0.50500.0")),
    .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.0.1")),
  ]
} else {
  package.dependencies += [
    .package(path: "../swift-syntax"),
    .package(path: "../swift-argument-parser"),
  ]
}
griffin_stewiegriffin_stewie

この対策をしても、Wait for the executable to be launched のオプションでのデバッグはうまく動かない。

griffin_stewiegriffin_stewie

swift-format コマンドのデバッグは Scheme の Arguments Passed On Launch のオプションに任意のコマンドを与えることで実行できる。

lint --recursive <lintをかけたいソースコードディレクトリへのフルパス>
griffin_stewiegriffin_stewie

Xcode でデバッグできるようになったのでやっと本題。ちなみにこれまでの Scrap は過去を振り返るように書いていたけどここからはほぼリアルタイム。

griffin_stewiegriffin_stewie

まず最初にブレイクポイントをしかけたのは、

apple/swift-format/Sources/SwiftFormatRules/ValidateDocumentationComments.swift
にある checkFunctionLikeDocumentation という関数。

  private func checkFunctionLikeDocumentation(
    _ node: DeclSyntax,
    name: String,
    parameters: FunctionParameterListSyntax,
    throwsOrRethrowsKeyword: TokenSyntax?,
    returnClause: ReturnClauseSyntax? = nil
  ) -> SyntaxVisitorContinueKind {
    guard let declComment = node.docComment else { return .skipChildren }
    guard let commentInfo = node.docCommentInfo else { return .skipChildren }
    guard let params = commentInfo.parameters else { return .skipChildren }

    // If a single sentence summary is the only documentation, parameter(s) and
    // returns tags may be ommitted.
    if commentInfo.oneSentenceSummary != nil && commentInfo.commentParagraphs!.isEmpty && params
      .isEmpty && commentInfo.returnsDescription == nil
    {
      return .skipChildren
    }

    // Indicates if the documentation uses 'Parameters' as description of the
    // documented parameters.
    let hasPluralDesc = declComment.components(separatedBy: .newlines).contains {
      $0.trimmingCharacters(in: .whitespaces).starts(with: "- Parameters")
    }

    validateThrows(
      throwsOrRethrowsKeyword, name: name, throwsDesc: commentInfo.throwsDescription, node: node)
    validateReturn(
      returnClause, name: name, returnDesc: commentInfo.returnsDescription, node: node)
    let funcParameters = funcParametersIdentifiers(in: parameters)

    // If the documentation of the parameters is wrong 'docCommentInfo' won't
    // parse the parameters correctly. First the documentation has to be fix
    // in order to validate the other conditions.
    if hasPluralDesc && funcParameters.count == 1 {
      diagnose(.useSingularParameter, on: node)
      return .skipChildren
    } else if !hasPluralDesc && funcParameters.count > 1 {
      diagnose(.usePluralParameters, on: node)
      return .skipChildren
    }

    // Ensures that the parameters of the documantation and the function signature
    // are the same.
    if (params.count != funcParameters.count) || !parametersAreEqual(
      params: params, funcParam: funcParameters)
    {
      diagnose(.parametersDontMatch(funcName: name), on: node)
    }

    return .skipChildren
  }

関数ボディーの3行目までは実際の処理に必要な情報を取り出せるかどうかを試して early return してる。
この情報群の名前を見る限り、どのように Doc Comment が解釈されているのかが分かりそう。

griffin_stewiegriffin_stewie

まずは、以下のコードをリントしてみる。これはリントツールで引っかかっていない部分なので問題ない正規の動作をしているはず。

/// ✅ This doc comment is OK
/// - Parameters:
///   - arg1: This is a arg1
///   - arg2: This is a arg2.
///
public func ok(arg1: String, arg2: String) {
    // do something
}

declComment ローカル変数

中身は以下の通り。

Printing description of declComment:
" ✅ This doc comment is OK\n - Parameters:\n   - arg1: This is a arg1\n   - arg2: This is a arg2.\n"

見た目的には、Swift で Doc Comment を表す/// が取り除かれた全体をただの String として入っているだけ。この時点で問題ない。

commentInfo ローカル変数

中身は以下の通り。

(lldb) po commentInfo
▿ ParseComment
  ▿ oneSentenceSummary : Optional<String>
    - some : " ✅ This doc comment is OK\n - Parameters:\n   - arg1: This is a arg1\n   - arg2: This is a arg2"
  ▿ commentParagraphs : Optional<Array<String>>
    - some : 0 elements
  ▿ parameters : Optional<Array<Parameter>>
    - some : 0 elements
  - throwsDescription : nil
  - returnsDescription : nil

これを初めて見たときには、「そもそもこの Doc Comment はリントで問題なしとなっているからこの出力結果が正だ」と思っていたけど、あとからみるとこの時点でおかしいことが分かる...

params ローカル変数

中身は以下の通り。

(lldb) po params
0 elements

はい。空っぽです。commentInfo が持つ parameteres プロパティの値が入っている変数なので、po commentInfo した時点で中身は分かってはいました。


後続の処理はここでは追いません。他のケースをみていきます。

griffin_stewiegriffin_stewie

まずは、以下のコードをリントしてみる。これはリントツールで引っかかっていない部分なので問題ない正規の動作をしているはず。

/// ✅ However this doc comment is OK.
/// - Parameters:
///   - arg1: This is a arg1.
///   - arg2: This is a arg2
public func ok2(arg1: String, arg2: String) {
    // do something
}

declComment ローカル変数

中身は以下の通り。

(lldb) po declComment
" ✅ However this doc comment is OK.\n - Parameters:\n   - arg1: This is a arg1.\n   - arg2: This is a arg2"

見た目的には、問題なし。全体を文字列として取得できている。

commentInfo ローカル変数

中身は以下の通り。

(lldb) po commentInfo
▿ ParseComment
  ▿ oneSentenceSummary : Optional<String>
    - some : " ✅ However this doc comment is OK"
  ▿ commentParagraphs : Optional<Array<String>>
    - some : 0 elements
  ▿ parameters : Optional<Array<Parameter>>
    ▿ some : 2 elements
      ▿ 0 : Parameter
        - name : "arg1"
        - summary : "This is a arg1."
      ▿ 1 : Parameter
        - name : "arg2"
        - summary : "This is a arg2"
  - throwsDescription : nil
  - returnsDescription : nil

ここで最初に試したケースと違いが出てきています。この commentInfo に入っている ParseComment 構造体の意味合いが見えてきました。

oneSentenceSummary は、Doc Comment の最初の概要説明が入ってそうな場所ですね。
commentParagraphs は、現段階ではまだピンときません。
parameters は、関数の引数に対するコメントが引数の数だけ配列で格納されているっぽいです。
他には、throw する関数だった場合の説明文が入ってそうな throwsDescription と戻り値がある場合の説明文が入っていそうな returnsDescription ですね。

params ローカル変数

中身は以下の通り。

(lldb) po params
▿ 2 elements
  ▿ 0 : Parameter
    - name : "arg1"
    - summary : "This is a arg1."
  ▿ 1 : Parameter
    - name : "arg2"
    - summary : "This is a arg2"

先にも書いたとおり commentInfo が持つ parameteres プロパティの値が入っている変数なので、po commentInfo した時点で中身は分かってはいました。

改めてこの値に注目すると、リント対象の関数の実際の引数の数とマッチしていることが分かります。

🤔

逆に考えると、最初に試したケース ってちゃんとリントされて居なかったんじゃないのか?

griffin_stewiegriffin_stewie

commentInfo ローカル変数に注目してみる。

リント対象

リント対象は以下のコード

/// This is sandbox。
/// - Parameters:
///   - arg1: This is a arg1
///   - arg2: This is a arg2
public func sandbox(arg1: String, arg2: String) throws {
    // do something
}

特徴としては

  • 最初の一行には . は含まれず日本語の区点 が使われている。
  • 他の箇所には . は含まれていない。
  • 関数自体は throws となっているのにその説明用のセクション - Throws: が記載されていない。

.swift-format の中身

使っている .swift-format の中身は以下の通り。特に記載がない限り、この Scrap でのリントはこの設定で行っている。

{
  "fileScopedDeclarationPrivacy" : {
    "accessLevel" : "private"
  },
  "indentation" : {
    "spaces" : 4
  },
  "indentConditionalCompilationBlocks" : true,
  "indentSwitchCaseLabels" : false,
  "lineBreakAroundMultilineExpressionChainComponents" : false,
  "lineBreakBeforeControlFlowKeywords" : false,
  "lineBreakBeforeEachArgument" : false,
  "lineBreakBeforeEachGenericRequirement" : false,
  "lineLength" : 300,
  "maximumBlankLines" : 2,
  "prioritizeKeepingFunctionOutputTogether" : false,
  "respectsExistingLineBreaks" : true,
  "rules" : {
    "AllPublicDeclarationsHaveDocumentation" : true,
    "AlwaysUseLowerCamelCase" : true,
    "AmbiguousTrailingClosureOverload" : true,
    "BeginDocumentationCommentWithOneLineSummary" : false,
    "DoNotUseSemicolons" : true,
    "DontRepeatTypeInStaticProperties" : true,
    "FileScopedDeclarationPrivacy" : true,
    "FullyIndirectEnum" : true,
    "GroupNumericLiterals" : true,
    "IdentifiersMustBeASCII" : true,
    "NeverForceUnwrap" : false,
    "NeverUseForceTry" : false,
    "NeverUseImplicitlyUnwrappedOptionals" : false,
    "NoAccessLevelOnExtensionDeclaration" : true,
    "NoBlockComments" : false,
    "NoCasesWithOnlyFallthrough" : true,
    "NoEmptyTrailingClosureParentheses" : true,
    "NoLabelsInCasePatterns" : true,
    "NoLeadingUnderscores" : false,
    "NoParensAroundConditions" : true,
    "NoVoidReturnOnFunctionSignature" : true,
    "OneCasePerLine" : true,
    "OneVariableDeclarationPerLine" : true,
    "OnlyOneTrailingClosureArgument" : true,
    "OrderedImports" : true,
    "ReturnVoidInsteadOfEmptyTuple" : true,
    "UseEarlyExits" : false,
    "UseLetInEveryBoundCaseVariable" : true,
    "UseShorthandTypeNames" : true,
    "UseSingleLinePropertyGetter" : true,
    "UseSynthesizedInitializer" : true,
    "UseTripleSlashForDocumentationComments" : true,
    "UseWhereClausesInForLoops" : true,
    "ValidateDocumentationComments" : true
  },
  "tabWidth" : 4,
  "version" : 1
}

リント結果

異常なしと判断される。

少なくとも throws の説明がないので警告が出るはずだったが、それがない。

何故、警告がでないのか中身を追ってみる

commentInfo ローカル変数の中身は node.docCommentInfo の戻り値。コードとしては DeclSyntaxProtocol+Comments.swift:81 に実装されている。

docCommentInfoプロパティの実装
  var docCommentInfo: ParseComment? {
    guard let docComment = self.docComment else { return nil }
    let comments = docComment.components(separatedBy: .newlines)
    var params = [ParseComment.Parameter]()
    var commentParagraphs = [String]()
    var currentSection: DocCommentSection = .commentParagraphs
    var returnsDescription: String?
    var throwsDescription: String?
    // Takes the first sentence of the comment, and counts the number of lines it uses.
    let oneSenteceSummary = docComment.components(separatedBy: ".").first
    let numOfOneSentenceLines = oneSenteceSummary!.components(separatedBy: .newlines).count

    // Iterates to all the comments after the one sentence summary to find the parameter(s)
    // return tags and get their description.
    for line in comments.dropFirst(numOfOneSentenceLines) {
      let trimmedLine = line.trimmingCharacters(in: .whitespaces)

      if trimmedLine.starts(with: "- Parameters") {
        currentSection = .parameters
      } else if trimmedLine.starts(with: "- Parameter") {
        // If it's only a parameter it's information is inline with the parameter
        // tag, just after the ':'.
        guard let index = trimmedLine.firstIndex(of: ":") else { continue }
        let name = trimmedLine.dropFirst("- Parameter".count)[..<index]
          .trimmingCharacters(in: .init(charactersIn: " -:"))
        let summary = trimmedLine[index...].trimmingCharacters(in: .init(charactersIn: " -:"))
        params.append(ParseComment.Parameter(name: name, summary: summary))
      } else if trimmedLine.starts(with: "- Throws:") {
        currentSection = .throwsDescription
        throwsDescription = String(trimmedLine.dropFirst("- Throws:".count))
      } else if trimmedLine.starts(with: "- Returns:") {
        currentSection = .returnsDescription
        returnsDescription = String(trimmedLine.dropFirst("- Returns:".count))
      } else {
        switch currentSection {
        case .parameters:
          // After the paramters tag is found the following lines should be the parameters
          // description.
          guard let index = trimmedLine.firstIndex(of: ":") else { continue }
          let name = trimmedLine[..<index].trimmingCharacters(in: .init(charactersIn: " -:"))
          let summary = trimmedLine[index...].trimmingCharacters(in: .init(charactersIn: " -:"))
          params.append(ParseComment.Parameter(name: name, summary: summary))

        case .returnsDescription:
          // Appends the return description that is not inline with the return tag.
          returnsDescription!.append(trimmedLine)

        case .throwsDescription:
          // Appends the throws description that is not inline with the throws tag.
          throwsDescription!.append(trimmedLine)

        case .commentParagraphs:
          if trimmedLine != "" {
            commentParagraphs.append(" " + trimmedLine)
          }
        }
      }
    }

docComment プロパティの中身はコメント全体の文字列。
これを改行文字で分割して comments ローカル変数に入れている。
ここまでは問題なし。

ここが諸悪の根源っぽい

    // Takes the first sentence of the comment, and counts the number of lines it uses.
    let oneSenteceSummary = docComment.components(separatedBy: ".").first
    let numOfOneSentenceLines = oneSenteceSummary!.components(separatedBy: .newlines).count

コメント全文を . で分割した最初の要素を oneSentenceSummary としている。
. が含まれていない文字列に .components(separatedBy: ".").first したあと oneSenteceSummary! して大丈夫かな?と思ったけど。分割対象文字がなかった場合は文字列全体が帰ってくるので nil にはなることがないみたい。

ちなみに各種ローカル変数と一部の処理を出力してみたらこんな感じ。

(lldb) po docComment
" This is sandbox。\n - Parameters:\n   - arg1: This is a arg1\n   - arg2: This is a arg2"
▿ 4 elements
  - 0 : " This is sandbox。"
  - 1 : " - Parameters:"
  - 2 : "   - arg1: This is a arg1"
  - 3 : "   - arg2: This is a arg2"
(lldb) po oneSenteceSummary
▿ Optional<String>
  - some : " This is sandbox。\n - Parameters:\n   - arg1: This is a arg1\n   - arg2: This is a arg2"
(lldb) po oneSenteceSummary!.components(separatedBy: .newlines)
▿ 4 elements
  - 0 : " This is sandbox。"
  - 1 : " - Parameters:"
  - 2 : "   - arg1: This is a arg1"
  - 3 : "   - arg2: This is a arg2"
(lldb) po comments.dropFirst(numOfOneSentenceLines) 
0 elements

この docCommentInfo は大体以下のようなことをしている。

  • 最初の 82行目部分でそもそも Doc Comment がなかれば nil
  • 95行目の for 文で各種値を組み立てていく前段の準備を 83行目〜94行目で行う。(ParseComment 構造体に渡すための情報を格納するローカル変数の初期化など)
  • 95行目の for 文でいろいろと処理(まだ中のコードは読んでいない)
  • 最終的に ParseComment 構造体に情報を詰めて返却。

今回のケースであれば、for 文の対象となる配列が 0 なので、前段の準備部分(83行目〜94行目)だけやって、ParseComment に詰めて返すしかしていない。

おそらく。
メソッドの説明要約部分を担ってもらうだろう oneSenteceSummary。これを取り出すための指標となる . が含まれない Doc Comment の場合は、Doc Comment 全体が oneSenteceSummary になってしまう。oneSenteceSummary を除いた残りの部分でパラメータや戻り値やThrowの説明文を収集して返却つもりだった。
oneSenteceSummary を正しくピックアップしない限りは、本来重要だと思われる以下の要素群は結果的に無視されてしまう。

  • 引数に関するリント
    • 引数が1つの時に - Parameters としていないか?
    • 引数が複数の時に - Parameter としていないか?
    • 引数のラベルの文字列やその数が実装とマッチしているか?
  • 戻り値がある場合、その説明があるか?
  • Throw する場合、その説明があるか?
griffin_stewiegriffin_stewie

大体分かってきたところで一旦まとめというか振り返り・おさらいしてみる。

最初に提示したOKとNGのコード例 の OK 例。

リント対象コード

/// ✅ This doc comment is OK
/// - Parameters:
///   - arg1: This is a arg1
///   - arg2: This is a arg2.
public func ok(arg1: String, arg2: String) {
    // do something
}

特徴は以下の通り

  • 1行目に . はない。
  • Parameters の使い方は正しいはず。
  • 引数名も数も合っている
  • 最後の引数の末尾に . がある
  • 戻り値も Throw もしていない。

リント結果

警告はゼロ。

内部動作

oneSenteceSummary として解釈されたのは

(lldb) po oneSenteceSummary
▿ Optional<String>
  - some : " ✅ This doc comment is OK\n - Parameters:\n   - arg1: This is a arg1\n   - arg2: This is a arg2"

for 文でチェックする対象(oneSenteceSummary以降の部分)は存在しない。

よって、引数のチェックは実施されていない。

結果

結局リントなんかしていない。

griffin_stewiegriffin_stewie

最初に提示したOKとNGのコード例 の NG 例。

リント対象コード

/// 🚫⚠️ This doc comment occurs swift-format lint warning [ValidateDocumentationComments]: change the parameters of doSomething's documentation to match its parameters
/// - Parameters:
///   - arg1: This is a arg1.
///   - arg2: This is a arg2
public func warning(arg1: String, arg2: String) {
    // do something
}

特徴は以下の通り

  • 1行目に . はない。
  • Parameters の使い方は正しいはず。
  • 引数名も数も合っている
  • 最初のの引数の末尾に . がある
  • 戻り値も Throw もしていない。

リント結果

warning: [ValidateDocumentationComments]: change the parameters of warning's documentation to match its parameters

内部動作

oneSenteceSummary として解釈されたのは

(lldb) po oneSenteceSummary
▿ Optional<String>
  - some : " 🚫⚠️ This doc comment occurs swift-format lint warning [ValidateDocumentationComments]: change the parameters of doSomething\'s documentation to match its parameters\n - Parameters:\n   - arg1: This is a arg1"

つまり、1つ目のパラメータの説明文の最後のピリオドまで含まれてしまっている。

for 文でチェックする対象(oneSenteceSummary以降の部分)は以下の通りの3要素。

(lldb) po oneSenteceSummary!.components(separatedBy: .newlines)
▿ 3 elements
  - 0 : " 🚫⚠️ This doc comment occurs swift-format lint warning [ValidateDocumentationComments]: change the parameters of doSomething\'s documentation to match its parameters"
  - 1 : " - Parameters:"
  - 2 : "   - arg1: This is a arg1"
(lldb) po comments
▿ 4 elements
  - 0 : " 🚫⚠️ This doc comment occurs swift-format lint warning [ValidateDocumentationComments]: change the parameters of doSomething\'s documentation to match its parameters"
  - 1 : " - Parameters:"
  - 2 : "   - arg1: This is a arg1."
  - 3 : "   - arg2: This is a arg2"

↑この comments から dropFirst(3) したものが for 文の対象になる。それは以下の通り。

(lldb) po comments.dropFirst(numOfOneSentenceLines)
▿ 1 element
  - 0 : "   - arg2: This is a arg2"

対象はパラメータの第2引数だけになっている。この時点でパラメータ数がメソッドの定義と合っていないという警告になるのは明白。

for 文の中の処理をみてみる

処理対象の文字列は、前後の空白文字が取り除かれて以下のようになっている。

(lldb) po trimmedLine
"- arg2: This is a arg2"

現在どのセクションを parse しているのかを記録しているローカル変数 currentSection は初期値の .commentParagraphs になっている。

if 文でいろんな条件に応じて処理をするようになっている。
パース対象としているセクションを表す以下の文字列で始まっていないので else 節に入る。(ちなみに Throws とかは末尾にコロンが入っているが、引数系には付いていないのはコロンがなくてもよいケースがあるからっぽい。)

  • - Parameters
  • - Parameter
  • - Throws:
  • - Returns:

else 節 ではローカル変数 currentSection によって switch 文で分岐されている。
今回は .commentParagraphs なので

        case .commentParagraphs:
          if trimmedLine != "" {
            commentParagraphs.append(" " + trimmedLine)
          }

文字列の配列なローカル変数 commentParagraphs に追加される。
for 文の対象は1つだけだったのでこのまま ParseComment 構造体に詰められて返却される。

返却される ParseComment の中身は

▿ ParseComment
  ▿ oneSentenceSummary : Optional<String>
    - some : " 🚫⚠️ This doc comment occurs swift-format lint warning [ValidateDocumentationComments]: change the parameters of doSomething\'s documentation to match its parameters\n - Parameters:\n   - arg1: This is a arg1"
  ▿ commentParagraphs : Optional<Array<String>>
    ▿ some : 1 element
      - 0 : " - arg2: This is a arg2"
  ▿ parameters : Optional<Array<Parameter>>
    - some : 0 elements
  - throwsDescription : nil
  - returnsDescription : nil

引数の説明が2つあったはずの Doc Comment が、1つ目の説明は oneSentenceSummary に吸い込まれ、2つ目の説明はコメントの段落として処理されてしまい、parameters 自体は空っぽ。

リント結果としては、関数の引数は2つあるのに、引数のコメントが0なので change the parameters of <関数名>'s documentation to match its parameters

結果

Doc Comment の解析がズタボロで、引数の説明を1つも書いてないことになっちゃっている....