├── .github └── workflows │ ├── continuous-integration.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── Documentation ├── Assets │ ├── Demo-Xcode.png │ ├── Demo.png │ ├── Logo-Circular-Header.png │ ├── Logo.sketch │ └── docstring-examples.gif ├── Configuration.md ├── EditorIntegrations.md ├── Explainers │ ├── E001.md │ ├── E002.md │ ├── E003.md │ ├── E004.md │ ├── E005.md │ ├── E006.md │ ├── E007.md │ ├── E008.md │ ├── E009.md │ ├── E010.md │ ├── E011.md │ ├── E012.md │ ├── E013.md │ ├── E014.md │ ├── E015.md │ ├── E016.md │ ├── E017.md │ ├── E018.md │ ├── E019.md │ ├── E020.md │ ├── README.md │ └── template.md ├── GettingStarted.md └── Overview.md ├── LICENSE.md ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md ├── Scripts ├── Dockerfile-5.9.2-focal ├── check-version.py ├── completions │ ├── bash │ │ └── drstring-completion.bash │ ├── drstring.fish │ └── zsh │ │ └── _drstring ├── generateexplainers.py ├── locateswift.sh ├── package-darwin.sh ├── ubuntu.sh └── ubuntuarchive.sh ├── Sources ├── Crawler │ ├── DocExtractor.swift │ └── SwiftSyntax+Extensions.swift ├── Critic │ ├── DocProblem.swift │ ├── Explainer.swift │ ├── Validating.swift │ └── explainers.swift ├── Decipher │ ├── LineParsing.swift │ ├── Parsing.swift │ └── StatefulParsing.swift ├── DrString │ └── main.swift ├── DrStringCLI │ ├── ConfigFileError.swift │ ├── interface.swift │ ├── overrides.swift │ └── run.swift ├── DrStringCore │ ├── Command.swift │ ├── Configuration.swift │ ├── Shims.swift │ ├── Timing.swift │ ├── check.swift │ ├── execute.swift │ ├── expandGlob.swift │ ├── explain.swift │ ├── extract.swift │ ├── format.swift │ └── version.swift ├── Editor │ ├── DocString+Formatting.swift │ ├── Documentable+Editing.swift │ └── Edit.swift ├── Informant │ ├── PlainTextFormatter.swift │ └── TtyTextFormatter.swift ├── Models │ ├── AbsoluteSourceLocation.swift │ ├── DocString.swift │ ├── Documentable.swift │ ├── ParameterStyle.swift │ ├── Section.swift │ ├── StringLeadByWhitespace.swift │ └── algorithms.swift └── _DrStringCore └── Tests ├── CLITests ├── CLITests.swift └── Fixtures │ └── config0.toml ├── CriticTests ├── AlignmentTests.swift ├── ParametersTests.swift ├── ReturnsTests.swift └── ThrowsTests.swift ├── DecipherTests ├── LineParsingTests.swift └── StatefulParsingTests.swift ├── DrStringCoreTests ├── EmptyPatternsTests.swift ├── Fixtures │ ├── 140.fixture │ ├── 147.fixture │ ├── Formatting │ │ ├── emptyitem.fixture │ │ ├── emptyitem_expectation.fixture │ │ ├── expectation0.fixture │ │ ├── expectation1.fixture │ │ ├── expectation192.fixture │ │ ├── expectation2.fixture │ │ ├── expectation4.fixture │ │ ├── source0.fixture │ │ ├── source1.fixture │ │ ├── source192.fixture │ │ ├── source2.fixture │ │ └── source4.fixture │ ├── alignAfterColon.fixture │ ├── alignAfterColonNotRequired.fixture │ ├── async.fixture │ ├── badParamFormat.fixture │ ├── badParametersKeyword.fixture │ ├── badReturnsFormat.fixture │ ├── badThrowsFormat.fixture │ ├── complete.fixture │ ├── groupedParameterStyle.fixture │ ├── ignoreReturns.fixture │ ├── ignoreThrows.fixture │ ├── init.fixture │ ├── lowercaseKeywords.fixture │ ├── misalignedParameterDescription.fixture │ ├── missingSectionSeparator.fixture │ ├── missingStuff.fixture │ ├── nodoc.fixture │ ├── positional.fixture │ ├── redundantKeywords.fixture │ ├── redundantKeywordsPathsOnly.fixture │ ├── separateParameterStyle.fixture │ ├── throwDescriptionNextLine.fixture │ ├── uppercaseKeywords.fixture │ └── whateverParameterStyle.fixture ├── FormattingTests.swift ├── InvalidPatternTests.swift ├── ProblemCheckingTests.swift └── SuperfluousExclusionTests.swift ├── EditorTests ├── DocStringDescriptionFormattingTests.swift ├── DocStringParameterFormattingTests.swift ├── DocStringReturnsFormattingTests.swift ├── DocStringSectionSeparationFormattingTests.swift ├── DocStringSeparatorTests.swift ├── DocStringThrowsFormattingTests.swift ├── FormatRangeTests.swift ├── LineFoldingTests.swift ├── ParameterPlaceholderTests.swift ├── ReturnsPlaceholderTests.swift └── ThrowsPlaceholderTests.swift └── ModelsTests └── LongestCommonSequenceTests.swift /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: [push] 4 | 5 | jobs: 6 | macos: 7 | name: macOS 8 | runs-on: macos-13 9 | strategy: 10 | matrix: 11 | action: 12 | - check-version 13 | - package-darwin 14 | - test 15 | - test-generated-artifacts 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Xcode version 19 | run: sudo xcode-select -s /Applications/Xcode_15.0.app 20 | - name: Action 21 | run: make ${{ matrix.action }} 22 | 23 | ubuntu: 24 | name: Ubuntu 25 | runs-on: ubuntu-latest 26 | strategy: 27 | matrix: 28 | ubuntu: 29 | - docker: amd64 30 | release: focal 31 | - docker: amd64 32 | release: jammy 33 | swift: 34 | - 5.9 35 | action: 36 | - build 37 | - test 38 | steps: 39 | - uses: actions/checkout@v2 40 | - name: Action 41 | run: Scripts/ubuntu.sh ${{ matrix.action }} ${{ matrix.swift }} ${{ matrix.ubuntu.release }} ${{ matrix.ubuntu.docker }} 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | darwin: 10 | name: Publish macOS binaries 11 | runs-on: macos-13 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Xcode version 15 | run: sudo xcode-select -s /Applications/Xcode_15.0.app 16 | - name: Build and package macOS executable 17 | run: make package-darwin 18 | - name: Upload binaries 19 | uses: svenstaro/upload-release-action@v1-release 20 | with: 21 | repo_token: ${{ secrets.GITHUB_TOKEN }} 22 | file: drstring_darwin.tar.gz 23 | asset_name: drstring-universal-apple-darwin.tar.gz 24 | tag: ${{ github.ref }} 25 | overwrite: true 26 | ubuntu: 27 | name: Publish Ubuntu binaries 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v2 31 | - name: Build and package Ubuntu executable 32 | run: make package-ubuntu 33 | - name: Upload binaries 34 | uses: svenstaro/upload-release-action@v1-release 35 | with: 36 | repo_token: ${{ secrets.GITHUB_TOKEN }} 37 | file: drstring-ubuntu-focal.tar.gz 38 | asset_name: drstring-x86_64-unknown-ubuntu.tar.gz 39 | tag: ${{ github.ref }} 40 | overwrite: true 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | .swiftpm/ 7 | .vscode 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | Scripts/Dockerfile-5.9.2-focal -------------------------------------------------------------------------------- /Documentation/Assets/Demo-Xcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dduan/DrString/1931434c29b3cc9e0a2b72e8d06dcc893b65de99/Documentation/Assets/Demo-Xcode.png -------------------------------------------------------------------------------- /Documentation/Assets/Demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dduan/DrString/1931434c29b3cc9e0a2b72e8d06dcc893b65de99/Documentation/Assets/Demo.png -------------------------------------------------------------------------------- /Documentation/Assets/Logo-Circular-Header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dduan/DrString/1931434c29b3cc9e0a2b72e8d06dcc893b65de99/Documentation/Assets/Logo-Circular-Header.png -------------------------------------------------------------------------------- /Documentation/Assets/Logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dduan/DrString/1931434c29b3cc9e0a2b72e8d06dcc893b65de99/Documentation/Assets/Logo.sketch -------------------------------------------------------------------------------- /Documentation/Assets/docstring-examples.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dduan/DrString/1931434c29b3cc9e0a2b72e8d06dcc893b65de99/Documentation/Assets/docstring-examples.gif -------------------------------------------------------------------------------- /Documentation/EditorIntegrations.md: -------------------------------------------------------------------------------- 1 | ## Xcode 2 | 3 | ### Formatting 4 | 5 | Use the [Xcode source editor extention][] implemented on top of DrString. 6 | 7 | [Xcode source editor extention]: https://apps.apple.com/us/app/drstring/id1523251484?mt=12 8 | 9 | ### Linting 10 | 11 | For linting, add a "Run Script" build phase in your project target: 12 | 13 | ```bash 14 | if which drstring >/dev/null; then 15 | drstring check --config-file "$SRCROOT/.drstring.toml" || true 16 | else 17 | echo "warning: DrString not installed. Run \ 18 | `brew install dduan/formulae/drstring` or learn more at \ 19 | https://github.com/dduan/DrString/blob/main/Documentation/GettingStarted.md#install" 20 | fi 21 | ``` 22 | 23 | Note the second command should be however you would run drstring in command 24 | line. `$SRCROOT` is a environment variable that mayb come in handy for locating 25 | your config file. 26 | 27 | ## Vim/NeoVim 28 | 29 | [dduan/DrString.vim](https://github.com/dduan/DrString.vim) provides Vim 30 | wrappers for DrString. 31 | 32 | ## Emacs 33 | 34 | [flycheck-drstring](https://github.com/danielmartin/flycheck-drstring) is 35 | a flycheck checker for Swift source code using DrString. 36 | -------------------------------------------------------------------------------- /Documentation/Explainers/E001.md: -------------------------------------------------------------------------------- 1 | ## E001: Redundant Parameter 2 | 3 | 4 | A documented parameter is not recognized as part of the function signature. 5 | 6 | 7 | ### Bad 8 | 9 | ```swift 10 | /// - parameter baz: description of bar 11 | func foo(bar: Int) 12 | ``` 13 | 14 | ### Good 15 | 16 | ```swift 17 | /// - parameter bar: description of bar 18 | func foo(bar: Int) 19 | ``` 20 | -------------------------------------------------------------------------------- /Documentation/Explainers/E002.md: -------------------------------------------------------------------------------- 1 | ## E002: Missing Parameter 2 | 3 | 4 | Documentation for a parameter found in the function signature is missing. 5 | 6 | 7 | ### Bad 8 | 9 | ```swift 10 | /// Overall description of foo, but no description of `bar` 11 | func foo(bar: Int) 12 | ``` 13 | 14 | ### Good 15 | 16 | ```swift 17 | /// Overall description of foo 18 | /// 19 | /// - parameter bar: description of bar 20 | func foo(bar: Int) 21 | ``` 22 | -------------------------------------------------------------------------------- /Documentation/Explainers/E003.md: -------------------------------------------------------------------------------- 1 | ## E003: Missing documentation for throws 2 | 3 | 4 | Function throws, but documentation for what it throws is missing. 5 | 6 | 7 | ### Bad 8 | 9 | ```swift 10 | /// Description 11 | /// 12 | /// - parameter bar: this is bar 13 | func foo(bar: Int) throws 14 | ``` 15 | 16 | ### Good 17 | 18 | ```swift 19 | /// Description 20 | /// 21 | /// - parameter bar: this is bar 22 | /// 23 | /// - throws: foo throws all kind of good stuff 24 | func foo(bar: Int) throws 25 | ``` 26 | -------------------------------------------------------------------------------- /Documentation/Explainers/E004.md: -------------------------------------------------------------------------------- 1 | ## E004: Missing documentation for returns 2 | 3 | 4 | Function returns a value, but documentation for what it returns is missing. 5 | 6 | 7 | ### Bad 8 | 9 | ```swift 10 | /// Documentation for return is missing 11 | /// 12 | /// - parameter bar: this is bar 13 | func foo(bar: Int) -> Int 14 | ``` 15 | 16 | ### Good 17 | 18 | ```swift 19 | /// Description 20 | /// 21 | /// - parameter bar: this is bar 22 | /// 23 | /// - returns: foo returns a good boi 24 | func foo(bar: Int) throws 25 | ``` 26 | -------------------------------------------------------------------------------- /Documentation/Explainers/E005.md: -------------------------------------------------------------------------------- 1 | ## E005: Space(s) before `-` 2 | 3 | 4 | Documentation for parameters, throws, returns, should start with exactly 1 space 5 | character before `-` if if. it's followed by a `parameter` keyword. If it's in 6 | a grouped list of parameters (followed by the parameter name), it must start 7 | with 3 space characters (so that its `-` is aligned with "P" in the 8 | ` - Parameters` header). Any other number of whitespaces is incorrect. 9 | 10 | 11 | ### Bad 12 | 13 | ```swift 14 | /// Description 15 | /// 16 | /// - parameter foo: too many spaces before `-` 17 | ///- parameter bar: no space before `-` is bad 18 | /// 19 | ///- throws: no good for throws neither 20 | /// 21 | /// - returns: weird 22 | func f(foo: Int, bar: Int) throws -> Int 23 | 24 | /// Description 25 | /// 26 | /// - parameters 27 | /// - foo: too many spaces before `-` 28 | ///- bar: no space before `-` 29 | /// - baz: two few spaces before `-` 30 | func g(foo: Int, bar: Int, baz: Int) 31 | ``` 32 | 33 | ### Good 34 | 35 | ```swift 36 | /// Description 37 | /// 38 | /// - parameter foo: 1 space 39 | /// - parameter bar: 1 space => happiness 40 | /// 41 | /// - throws: nice 42 | /// 43 | /// - returns: 1 space 44 | func f(foo: Int, bar: Int) throws -> Int 45 | 46 | /// Description 47 | /// 48 | /// - parameters 49 | /// - foo: Notice how 50 | /// - bar: all the `-`s 51 | /// - baz: are lined up ventically with "p" 52 | func g(foo: Int, bar: Int, baz: Int) 53 | ``` 54 | -------------------------------------------------------------------------------- /Documentation/Explainers/E006.md: -------------------------------------------------------------------------------- 1 | ## E006: Space character between `-` and keyword 2 | 3 | 4 | If a `- parameter`, `- throws`, or `- returns` starts docstring, there must be 5 | exactly 1 space character between `-` and the first letter. 6 | 7 | 8 | ### Bad 9 | 10 | ```swift 11 | /// Description 12 | /// 13 | /// -parameter foo: no space, bad 14 | /// - parameter bar: two many spaces, bad 15 | /// 16 | /// -throws: no good for throws neither 17 | /// 18 | /// - returns: weird 19 | func f(foo: Int, bar: Int) throws -> Int 20 | ``` 21 | 22 | ### Good 23 | 24 | ```swift 25 | /// Description 26 | /// 27 | /// - parameter foo: one space character! 28 | /// - parameter bar: this is good. 29 | /// 30 | /// - throws: great 31 | /// 32 | /// - returns: looks good 33 | func f(foo: Int, bar: Int) throws -> Int 34 | ``` 35 | -------------------------------------------------------------------------------- /Documentation/Explainers/E007.md: -------------------------------------------------------------------------------- 1 | ## E007: Space before parameter name 2 | 3 | 4 | Exactly 1 whitespace preceeds name of the parameter in its docstring. 5 | 6 | 7 | ### Bad 8 | 9 | ```swift 10 | /// Description 11 | /// 12 | /// - parameter foo: too many spaces 13 | func f(foo: Int) 14 | 15 | /// Description 16 | /// 17 | /// - parameters 18 | /// - foo: too many spaces! 19 | /// - bar: not quite right! 20 | func f(foo: Int, bar: Int) 21 | ``` 22 | 23 | ### Good 24 | 25 | ```swift 26 | /// Description 27 | /// 28 | /// - parameter foo: good 29 | func f(foo: Int) 30 | 31 | /// Description 32 | /// 33 | /// - parameters 34 | /// - foo: good 35 | /// - bar: very good 36 | func f(foo: Int, bar: Int) 37 | ``` 38 | -------------------------------------------------------------------------------- /Documentation/Explainers/E008.md: -------------------------------------------------------------------------------- 1 | ## E008: No space before `:` 2 | 3 | 4 | In documentation for parameters, throws, and returns, the `:` before their 5 | description must have any whitespace characters between it and the word before 6 | it. 7 | 8 | 9 | ### Bad 10 | 11 | ```swift 12 | /// Description 13 | /// 14 | /// - parameter foo : oh no, there's a space before `:` 15 | /// 16 | /// - throws : this is no good for throws 17 | /// 18 | /// - returns : more spaces isn't better 19 | func f(foo: Int) throws -> Int 20 | ``` 21 | 22 | ### Good 23 | 24 | ```swift 25 | /// Description 26 | /// 27 | /// - parameter foo: 1 space 28 | /// 29 | /// - throws: the best 30 | /// 31 | /// - returns: yay 32 | func f(foo: Int) throws -> Int 33 | ``` 34 | -------------------------------------------------------------------------------- /Documentation/Explainers/E009.md: -------------------------------------------------------------------------------- 1 | ## E009: Vertical-alignment of descriptions 2 | 3 | 4 | Each line of description should start at the same column or later. All 5 | descriptions of parameters should start on the same column dictated by the 6 | paramter with the longest name. 7 | 8 | 9 | ### Bad 10 | 11 | ```swift 12 | /// Description 13 | /// 14 | /// - parameter foo: 1 space after `:` is not enough because it's not aligned 15 | /// with `barbaz`. Also, this line started too early 16 | /// - parameter barbaz: but starting in later columns in continued lines is 17 | /// fine! 18 | func f(foo: Int, barbaz: Int) 19 | ``` 20 | 21 | ### Good 22 | 23 | ```swift 24 | /// Description 25 | /// 26 | /// - parameter foo: Notice how everything is vertically aligned 27 | /// even for coninuation lines like this one 28 | /// - parameter barbaz: Description 29 | /// of 30 | /// barbaz 31 | func f(foo: Int, barbaz: Int) 32 | ``` 33 | -------------------------------------------------------------------------------- /Documentation/Explainers/E010.md: -------------------------------------------------------------------------------- 1 | ## E010: Space after `:` for throws and returns 2 | 3 | 4 | Description for throws and returns should start with 1 space character after 5 | `:`. 6 | 7 | 8 | ### Bad 9 | 10 | ```swift 11 | /// Description 12 | /// 13 | /// - throws: One space too many after `:` 14 | /// 15 | /// - returns:No space no bueno. 16 | func f() throws -> Int 17 | ``` 18 | 19 | ### Good 20 | 21 | ```swift 22 | /// Description 23 | /// 24 | /// - throws: One space :) 25 | /// 26 | /// - returns: <= Best space 27 | func f() throws -> Int 28 | ``` 29 | -------------------------------------------------------------------------------- /Documentation/Explainers/E011.md: -------------------------------------------------------------------------------- 1 | ## E011: Correct spelling for keywords 2 | 3 | 4 | Keywords such as `Parameters`, `Throws`, and `Returns` must be spelled 5 | correctly. Their initial letter must be uppercased or lowercased depending on 6 | _convention_ specified for the project. 7 | 8 | 9 | ### Bad 10 | 11 | ```swift 12 | /// Let's say the convention is lower case 13 | /// 14 | /// - Parameter bar: violation! Convention dictates `parameter` 15 | /// - parameterz baz: mis-spelled. 16 | /// 17 | /// - return: spelling is wrong, missed an "s" at the end 18 | func foo(bar: Int, baz: Int) -> Int 19 | ``` 20 | 21 | ### Good 22 | 23 | ```swift 24 | /// Let's say the convention is lower case 25 | /// 26 | /// - parameter bar: good 27 | /// - parameter baz: nice 28 | /// 29 | /// - returns: got it 30 | func foo(bar: Int, baz: Int) -> Int 31 | ``` 32 | -------------------------------------------------------------------------------- /Documentation/Explainers/E012.md: -------------------------------------------------------------------------------- 1 | ## E012: Documentation section should end with empty line 2 | 3 | 4 | Document sections (overall description, parameters, throws) should include an 5 | empty comment line at its end to separate it and the next section. 6 | 7 | 8 | ### Bad 9 | 10 | ```swift 11 | /// Overall description 12 | /// it continues. 13 | /// - Parameters 14 | /// - first: description for first parameter 15 | /// - second: description for first parameter 16 | /// - Throws: what gets thrown 17 | /// - Returns: good stuff 18 | ``` 19 | 20 | ### Good 21 | 22 | ```swift 23 | /// Overall description 24 | /// it continues. 25 | /// 26 | /// - Parameters 27 | /// - first: description for first parameter 28 | /// - second: description for first parameter 29 | /// 30 | /// - Throws: what gets thrown 31 | /// 32 | /// - Returns: good stuff 33 | ``` 34 | -------------------------------------------------------------------------------- /Documentation/Explainers/E013.md: -------------------------------------------------------------------------------- 1 | ## E013: Redundant docstring for `throws` or `returns` 2 | 3 | 4 | Function signature does not throw yet a docstring for throw is present. Or, 5 | function doesn't return anything yet a docstring for return is present. 6 | 7 | 8 | ### Bad 9 | 10 | ```swift 11 | /// Description 12 | /// - parameter foo: description for foo 13 | /// - throws: throws some things? 14 | /// - returns: returns otherthings? 15 | func f(foo: Int) 16 | ``` 17 | 18 | ### Good 19 | 20 | ```swift 21 | /// Description 22 | /// - parameter foo: description for foo 23 | func f(foo: Int) 24 | ``` 25 | -------------------------------------------------------------------------------- /Documentation/Explainers/E014.md: -------------------------------------------------------------------------------- 1 | ## E014: Redundant text in parameter headers 2 | 3 | 4 | There should be no text following the parameter group header `- Parameters:` in 5 | grouped style parameter documentation. 6 | 7 | 8 | ### Bad 9 | 10 | ```swift 11 | /// Description 12 | /// 13 | /// - Parameters: <- nothing should be here 14 | /// - foo: description of foo 15 | /// - bar: description of bar 16 | func f(foo: Int, bar Int) 17 | ``` 18 | 19 | ### Good 20 | 21 | ```swift 22 | /// Description 23 | /// 24 | /// - Parameters: 25 | /// - foo: description of foo 26 | /// - bar: description of bar 27 | func f(foo: Int, bar Int) 28 | ``` 29 | -------------------------------------------------------------------------------- /Documentation/Explainers/E015.md: -------------------------------------------------------------------------------- 1 | ## E015: Excluded file has no docstring problems. 2 | 3 | 4 | The full path for this file is in the excluded list, however, DrString couldn't 5 | find any problems in the file. It should be removed from the exclusion list. 6 | -------------------------------------------------------------------------------- /Documentation/Explainers/E016.md: -------------------------------------------------------------------------------- 1 | ## E016: Excluded path is not included to begin with 2 | 3 | 4 | The full path for this file is in the excluded list, however, it's not part of 5 | the included paths and won't be checked anyways. It should be removed from the 6 | exclusion list. 7 | -------------------------------------------------------------------------------- /Documentation/Explainers/E017.md: -------------------------------------------------------------------------------- 1 | ## E017: Parameters are expecetd to be grouped 2 | 3 | 4 | Parameters are preferred to be organized in the "grouped" style, with the section 5 | starting with ` - Parameters:` and followed by entries for each parameter. The 6 | entry should look like ` - parameterName: description ...`. 7 | 8 | The header is not required if only one parameter exists. 9 | 10 | 11 | ### Bad 12 | 13 | ```swift 14 | /// Description 15 | /// 16 | /// - Parameter foo: description of foo 17 | /// - Parameter bar: description of bar 18 | func f(foo: Int, bar: Int) 19 | ``` 20 | 21 | ### Good 22 | 23 | ```swift 24 | /// Description 25 | /// 26 | /// - Parameters: 27 | /// - foo: description of foo 28 | /// - bar: description of bar 29 | func f(foo: Int, bar: Int) 30 | 31 | /// Description 32 | /// 33 | /// - Parameter foo: description of foo 34 | func g(baz: Int) 35 | ``` 36 | -------------------------------------------------------------------------------- /Documentation/Explainers/E018.md: -------------------------------------------------------------------------------- 1 | ## E018: Parameters are expecetd to be separate 2 | 3 | 4 | Parameters are preferred to be organized in the "separate" style. Each entry for 5 | a parameter should begin with ` - Parameter`, followed by the parameter name, 6 | a colon and the description. 7 | 8 | 9 | ### Bad 10 | 11 | ```swift 12 | /// Description 13 | /// 14 | /// - Parameters: 15 | /// - foo: description of foo 16 | /// - bar: description of bar 17 | func f(foo: Int, bar: Int) 18 | ``` 19 | 20 | ### Good 21 | 22 | ```swift 23 | /// Description 24 | /// 25 | /// - Parameter foo: description of foo 26 | /// - Parameter bar: description of bar 27 | func f(foo: Int, bar: Int) 28 | ``` 29 | -------------------------------------------------------------------------------- /Documentation/Explainers/E019.md: -------------------------------------------------------------------------------- 1 | ## E019: Missing Colon In Docstring Entry 2 | 3 | 4 | Docstring entries for parameters, returns, throws, etc, should begin with 5 | a header such as `- returns:` with a colon character at the end. 6 | 7 | 8 | ### Bad 9 | 10 | ```swift 11 | /// Some function 12 | /// 13 | /// - returns The answer to life, universe, and everything. 14 | 15 | ``` 16 | 17 | ### Good 18 | 19 | ```swift 20 | /// Some function 21 | /// 22 | /// - returns: The answer to life, universe, and everything. 23 | ``` 24 | -------------------------------------------------------------------------------- /Documentation/Explainers/E020.md: -------------------------------------------------------------------------------- 1 | ## E020: Invalid inclusion/exclusion pattern 2 | 3 | 4 | Patterns specified for included/excluded paths must match one or more files on 5 | disk. Otherwise it is invalid. 6 | -------------------------------------------------------------------------------- /Documentation/Explainers/README.md: -------------------------------------------------------------------------------- 1 | # DrString Explainers 2 | 3 | Files in this folder is structured data. 4 | 5 | Each error reported by the `check` command is associated with a explanation. Each file in this folder (except 6 | this one and template.md) is named after the ID of an explanation. The command `explain` uses content of these 7 | files as data source. Therefore, it's vital that they follow the set structure. 8 | 9 | 10 | ## The Structure 11 | 12 | Each explainer is composed of the following _section_ s: 13 | 14 | * Title 15 | * Description 16 | * Bad example and a good example (optional) 17 | 18 | Two empty lines separate each _section_. 19 | 20 | The title must be a H2. 21 | 22 | The description can have manylines but no two consecutive empty lines. 23 | 24 | The examples start with a H3 saying "bad" or "good", followed by an empty line, followed by a block quoted 25 | example "code". Examples are optional except both must be present or missing at the same time. 26 | 27 | See [template](template.md) for an example. 28 | -------------------------------------------------------------------------------- /Documentation/Explainers/template.md: -------------------------------------------------------------------------------- 1 | ## E000: Template, This is a title 2 | 3 | 4 | This is the summary. 5 | It can have many lines. 6 | Note how it's separated from the title by two blank lines. 7 | 8 | 9 | ### Bad 10 | 11 | ```swift 12 | // Here goes an example. 13 | ``` 14 | 15 | ### Good 16 | 17 | ```swift 18 | // Good example is at the *same* section as bad, so there's 1 blank line between its header and the end of 19 | // bad example's code block. 20 | ``` 21 | -------------------------------------------------------------------------------- /Documentation/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting Started with DrString 2 | 3 | This tutorial teaches the basics of installing and using DrString. 4 | 5 | Read [overview][] if you aren't sure about using DrString. 6 | 7 | This tutorial assumes that you are comfortable with the command line. If not, 8 | then read this introduction to the command line: 9 | 10 | - [Learn Enough Command Line to Be Dangerous][cli] 11 | 12 | [cli]: https://www.learnenough.com/command-line-tutorial 13 | 14 | [overview]: Overview.md 15 | 16 | ## Install 17 | 18 | ### With Homebrew 19 | 20 | ```bash 21 | brew install dduan/formulae/drstring 22 | ``` 23 | 24 | ### With [Mint](https://github.com/yonaskolb/Mint) 25 | 26 | ``` 27 | mint install dduan/DrString 28 | ``` 29 | 30 | ### Download from [release page](https://github.com/dduan/DrString/releases) 31 | 32 | For macOS and Ubuntu Bionic/Focal, you can download a package from a release. 33 | After unarchiving the package, the `drstring` binary should run without problems. 34 | 35 | For Ubuntu, Make sure all content of the archive stay in the same relative 36 | location when you move them. For example, if you move the executable to 37 | `FOO/bin/drstring`, the content in `lib/*` should be in `FOO/lib`. 38 | 39 | If a parent process who calls `drstring` set the environment variable 40 | `LD_LIBRARY_PATH` on Linux, it may affect its successful execution. You must 41 | set `LD_LIBRARY_PATH` to the `lib` directory distributed with `drstring` in its 42 | execution environment to restore its functionality. 43 | 44 | ## Usage 45 | 46 | DrString is a CLI tool with subcommands. Some subcommands include many options 47 | that customizes its behavior. These options can be expressed via either command 48 | line arguments, or a configuration file. 49 | 50 | ### Checking for docstring problems 51 | 52 | The `check` subcommand finds problems of existing docstrings: 53 | 54 | ```bash 55 | drstring check -i 'Sources/**/*.swift' 56 | ``` 57 | 58 | In this example, all Swift files under `./Sources` will be examined recursively. 59 | Paths is the only required option for the `check` command. 60 | 61 | Another example: 62 | 63 | ```bash 64 | drstring check \ 65 | --include 'Sources/**/*.swift' \ 66 | --include 'Tests/**/*.swift' \ 67 | --exclude 'Tests/Fixtures/*.swift' \ 68 | --ignore-throws \ 69 | --first-letter uppercase 70 | ``` 71 | 72 | 1. `--include` is the longer form of `-i`. Some options have a long form and 73 | a short form. This should be a familar Unix pattern. 74 | 2. You can exampt paths from being checked with `--exclude` (or `-e`). 75 | 3. `--include` and `--exclude` can repeat. In fact, this is true for all options 76 | that take a list of values. 77 | 4. `--ignore-throws` tells DrString documentation for `throws` is not required. 78 | `--first-letter` tells DrString to expect keywords in docstring such as 79 | `Parameter` should start with an uppercase letter. These are examples of 80 | options that customizes DrString's behavior. They exist to cater different 81 | style needs in different codebases. 82 | 83 | There are many more [options][] for customizing DrString's behavior. 84 | 85 | ### Explainers 86 | 87 | An output of the `check` command may look like this: 88 | 89 | ![](Assets/Demo.png) 90 | 91 | You may have noticed some texts before each problem that reads like `|E008|`. 92 | This is an ID for this category of problems. DrString has an `explain` 93 | subcommand that takes the ID as argument: 94 | 95 | ```bash 96 | drstring explain E008 97 | ``` 98 | 99 | In this example, DrString will proceed to print out an explanation of the 100 | problem with examples that demonstrates the violation. 101 | 102 | 103 | ### Using a config file 104 | 105 | Instead of specifying request with command line arguments, DrString can read 106 | from a configuration file. The second example for `check` command can be 107 | expressed in a configuration file as 108 | 109 | ```toml 110 | include = [ 111 | 'Sources/**/*.swift', 112 | 'Tests/**/*.swift', 113 | ] 114 | 115 | exclude = [ 116 | 'Tests/Fixtures/*.swift', 117 | ] 118 | 119 | ignore-throws = true 120 | first-letter = "uppercase" 121 | ``` 122 | 123 | Save this file as `.drstring.toml` in the current worknig directory and simply 124 | run `drstring` will cause DrString to do the exact same thing as the CLI 125 | argument examlpe with `check` subcommand. 126 | 127 | Different location for the config file can be specified as a command-line 128 | argument via `--config-file`. For example: 129 | 130 | ```bash 131 | drstring format --config-file PATH_TO_CONFIG_TOML 132 | ``` 133 | 134 | When the config file is not explicitly specified, but at least one file path is 135 | present as a command-line argument, DrString will look for `.drstring.toml` in 136 | its directory. If it's not there, then its parent directory… until the config 137 | file is found, or root directory is encountered. 138 | 139 | The configuration file is in [TOML][] format. 140 | 141 | Options from command-line arguments overrides those found in config files. 142 | 143 | [TOML]: https://github.com/toml-lang/toml 144 | 145 | ### Automatically fix whitespace errors 146 | 147 | The `format` subcommand finds and fixes formatting errors in your docstrings. 148 | 149 | ```bash 150 | drstring format -i 'Sources/**/*.swift' 151 | ``` 152 | 153 | It shares most [options][] with the `check` subcommand, which can be specified 154 | as command-line arguments or via the config file. 155 | 156 | [options]: Configuration.md 157 | 158 | ### Extract docstrings in JSON 159 | 160 | Sometimes it's desirable to process existing docstring yourself. With the 161 | `extract` subcommand, DrString output the signature and associated docstring 162 | in JSON format, so that you can do whatever you like. 163 | 164 | This subcommand uses the same information as `check`, and `format` to determine 165 | the list of files to extract from. 166 | 167 | ### Integration with Xcode 168 | 169 | Add a "Run Script" build phase in your project target: 170 | 171 | ```bash 172 | if which drstring >/dev/null; then 173 | drstring check --config-file "$SRCROOT/.drstring.toml" || true 174 | else 175 | echo "warning: DrString not installed. Run \ 176 | `brew install dduan/formulae/drstring` or learn more at \ 177 | https://github.com/dduan/DrString/blob/main/Documentation/GettingStarted.md#install" 178 | fi 179 | ``` 180 | 181 | Note the second command should be however you would run drstring in command 182 | line. `$SRCROOT` is a environment variable that mayb come in handy for locating 183 | your config file. 184 | 185 | There's a [Source editor extension][] that generates, and reformats comments in Xcode on 186 | demand. 187 | 188 | [Source editor extension]: https://apps.apple.com/us/app/drstring/id1523251484?mt=12 189 | 190 | ### Getting help 191 | 192 | `-h` or `--help` is an option for all subcommands as well as the "root" command. 193 | It's good for a quick reminder of what's available. 194 | 195 | Read the [documentation for options][options] to learn more about ways to 196 | enforec different docstring styles. 197 | 198 | [options]: Configuration.md 199 | 200 | ### Tips and tricks 201 | 202 | #### Explain faster 203 | 204 | For the `explain`subcommand You can use partial IDs and DrString will try its 205 | best to guess what you want to know. For example, instead of typing `E008`, `E8` 206 | or `8` or `008` all get you the same result. 207 | 208 | #### Starting off in a big codebase 209 | 210 | In a big project, DrString might complain a lot at time of introduction. It's 211 | totally reasonable to exclude files that contains these problems to begin with 212 | (and slowly fix the problems, of course. How? I'll leave that as a reader 213 | exercise…) 214 | 215 | DrString has a `paths` format that outputs only paths to the problematic files: 216 | 217 | ```bash 218 | drstring check --format paths 219 | ``` 220 | 221 | With some light processing, this can become the list of paths to exclude. 222 | 223 | #### Running in CI 224 | 225 | `drstring check` exits with status code 0 when no docstring problem is found, 226 | and non-zero otherwise. Description of problems is printed to stdout and 227 | a summary of problem ("found N problems in M files...") is printed in stderr. 228 | 229 | This information should help you collect signal for failure, logs, etc. 230 | 231 | #### Negative flags 232 | 233 | On the command line, if `--x` means `true` for an option, `--no-x` can be used 234 | for the corresponding `false` value. 235 | 236 | ### Shell Completion Script 237 | 238 | DrString has a lot of CLI options! You may install one of the completion scripts 239 | included in the project if one exists for your shell. You can find them 240 | [here](../Scripts/completions). 241 | -------------------------------------------------------------------------------- /Documentation/Overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This document answers the following questions: 4 | 5 | - Why would one need something like DrString? 6 | - What's in a docstring? 7 | 8 | ## Consistency is hard 9 | 10 | [Docstring][] is an important part of a codebase. 11 | 12 | Just like all forms of documentation, it goes out of date as code evolves. Even 13 | with the most conscientious team and best of intentions, keeping it 100% fresh 14 | is time-consuming and difficult. 15 | 16 | There are many subtle ways a docstring can be "correct": 17 | 18 | ![](Assets/docstring-examples.gif) 19 | 20 | Picking a style and sticking to it is nice, but spending time in code reviews 21 | to maintain a consistent style is a trade of much efforts with low rewards. 22 | 23 | Finally, just like code formats, whitespace errors happens all the time. 24 | 25 | For these reason, docstring consistency is essentially impossible. 26 | A linter/formatter like DrString makes life a bit easier for those who cares. 27 | 28 | DrString recognize not all codebases adhere to the same style. Therefore, it 29 | offers many [options](Configuration.md) that customizes its behavior to suit 30 | your needs. 31 | 32 | [Docstring]: https://en.wikipedia.org/wiki/Docstring 33 | 34 | ## Anatomy of a docstring in Swift 35 | 36 | Pedantry begins with a glossary. So let's look at what elements of a docstring 37 | we can scrutinize about. 38 | 39 | A docstring is a block of consecutive lines that all starts with `///`. The 40 | first `/` is indented to the same column as its "documentee". 41 | 42 | Each parts of a signature corresponds to a section. This includes 43 | - an overall description of the signature 44 | - a description for each parameter 45 | - a description for what it throws if it throws anything 46 | - a description for what it returns, if anything 47 | 48 | Sometimes sections are separated with a empty docstring line. Some codebases 49 | don't care about certain sections, such as `throws`. 50 | 51 | The keywords "Parameters", "Parameter", "Throws", and "Returns" serve as 52 | prefixes to parts of the docstring. They should be spelled correctly. Their 53 | first letter could be upper- or lowercase by preference, and it's a source of 54 | inconsistency. 55 | 56 | Auxillary characters such as `-` and `:`. 57 | 58 | Space characters (and lack there of) surrounding various parts. Most of the time 59 | this is pretty strict ("no space before `:`", "1 space after `-`", etc). They 60 | make things visually align vertically. 61 | 62 | There are 2 common ways to organize description for multiple parameters (thanks 63 | to Xcode changing its generated docstring over the years). 64 | 65 | - The _grouped_ style starts the section with a `- Parameters` header in its own 66 | line. And each parameter following it is indented, and doesn't need the 67 | `Parameter` keyword. 68 | - The _separate_ style doesn't have a header for parameters. Each parameter 69 | begins with ` - Parameter` or ` - parameter`. 70 | 71 | DrString analyzes all of these aspects of your docstring and reports each 72 | problem it finds. Each reported problem comes with an explanation linked by an 73 | identifier. The "explainers" are part of the CLI app as well as the 74 | [documentation](https://github.com/dduan/DrString/tree/main/Documentation/Explainers). 75 | 76 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 DrString contributors 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | 3 | ifeq ($(shell uname),Darwin) 4 | EXTRA_SWIFT_FLAGS = --arch arm64 --arch x86_64 --disable-sandbox -Xlinker -dead_strip 5 | else 6 | SWIFT_TOOLCHAIN = $(shell dirname $(shell ./Scripts/locateswift.sh)) 7 | EXTRA_SWIFT_FLAGS = -Xswiftc -static-stdlib -Xlinker -fuse-ld=lld -Xlinker -L${SWIFT_TOOLCHAIN}/swift/linux -Xlinker -rpath -Xlinker '$$ORIGIN/../lib' 8 | endif 9 | 10 | .PHONY: test 11 | test: 12 | @swift test 13 | 14 | .PHONY: check-version 15 | check-version: 16 | @Scripts/check-version.py 17 | 18 | .PHONY: test-generated-artifacts 19 | test-generated-artifacts: 20 | @$(MAKE) generate 21 | @git diff --exit-code 22 | 23 | .PHONY: build 24 | build: 25 | @swift build --configuration release -Xswiftc -warnings-as-errors ${EXTRA_SWIFT_FLAGS} 26 | 27 | .PHONY: generate 28 | generate: generate-explainers generate-completion-scripts 29 | 30 | .PHONY: generate-explainers 31 | generate-explainers: 32 | @Scripts/generateexplainers.py 'Documentation/Explainers' > Sources/Critic/explainers.swift 33 | 34 | .PHONY: generate-completion-scripts 35 | generate-completion-scripts: 36 | @swift run drstring --generate-completion-script zsh > Scripts/completions/zsh/_drstring 37 | @swift run drstring --generate-completion-script bash > Scripts/completions/bash/drstring-completion.bash 38 | @swift run drstring --generate-completion-script fish > Scripts/completions/drstring.fish 39 | 40 | .PHONY: build-docker 41 | build-docker: 42 | @docker build --platform linux/amd64 --force-rm --tag drstring . 43 | 44 | .PHONY: package-darwin 45 | package-darwin: build 46 | @Scripts/package-darwin.sh 47 | 48 | .PHONY: package-ubuntu 49 | package-ubuntu: 50 | @Scripts/ubuntuarchive.sh 5.9.2 focal amd64 51 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "chalk", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/mxcl/Chalk.git", 7 | "state" : { 8 | "revision" : "9aa9f348b86db3cf6702a3e43c081ecec80cf3c7", 9 | "version" : "0.4.0" 10 | } 11 | }, 12 | { 13 | "identity" : "filecheck", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/llvm-swift/FileCheck.git", 16 | "state" : { 17 | "revision" : "f7c5f1a9479b33a876a6f5632ca2b92a7ce4b667", 18 | "version" : "0.2.6" 19 | } 20 | }, 21 | { 22 | "identity" : "istty", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/dduan/IsTTY.git", 25 | "state" : { 26 | "revision" : "7d4bc7788580c99d5a5295bdde96d0264b06d96e", 27 | "version" : "0.1.0" 28 | } 29 | }, 30 | { 31 | "identity" : "pathos", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/dduan/Pathos.git", 34 | "state" : { 35 | "revision" : "8697a340a25e9974d4bbdee80a4c361c74963c00", 36 | "version" : "0.4.2" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-argument-parser", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-argument-parser", 43 | "state" : { 44 | "revision" : "f3c9084a71ef4376f2fabbdf1d3d90a49f1fabdb", 45 | "version" : "1.1.2" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-syntax", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-syntax", 52 | "state" : { 53 | "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", 54 | "version" : "509.1.1" 55 | } 56 | }, 57 | { 58 | "identity" : "tomldecoder", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/dduan/TOMLDecoder.git", 61 | "state" : { 62 | "revision" : "a70a127cab92ddcb562a548190fa3ad2a2e5f9bc", 63 | "version" : "0.2.2" 64 | } 65 | } 66 | ], 67 | "version" : 2 68 | } 69 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.6 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "DrString", 6 | platforms: [ 7 | .macOS(.v10_15), 8 | ], 9 | products: [ 10 | .executable( 11 | name: "drstring", 12 | targets: ["DrString"] 13 | ), 14 | .library( 15 | name: "DrStringCore", 16 | targets: ["DrStringCore"] 17 | ), 18 | ], 19 | dependencies: [ 20 | .package( 21 | url: "https://github.com/apple/swift-syntax.git", 22 | exact: "509.1.1" 23 | ), 24 | .package( 25 | url: "https://github.com/dduan/IsTTY.git", 26 | exact: "0.1.0" 27 | ), 28 | .package( 29 | url: "https://github.com/dduan/Pathos.git", 30 | exact: "0.4.2" 31 | ), 32 | .package( 33 | url: "https://github.com/dduan/TOMLDecoder.git", 34 | exact: "0.2.2" 35 | ), 36 | .package( 37 | url: "https://github.com/mxcl/Chalk.git", 38 | exact: "0.4.0" 39 | ), 40 | .package( 41 | url: "https://github.com/apple/swift-argument-parser", 42 | exact: "1.1.2" 43 | ), 44 | 45 | // For testing 46 | .package( 47 | url: "https://github.com/llvm-swift/FileCheck.git", 48 | exact: "0.2.6" 49 | ), 50 | ], 51 | targets: [ 52 | .target( 53 | name: "Crawler", 54 | dependencies: [ 55 | "Decipher", 56 | "Pathos", 57 | .product(name: "SwiftParser", package: "swift-syntax"), 58 | .product(name: "SwiftSyntax", package: "swift-syntax"), 59 | ] 60 | ), 61 | .target( 62 | name: "Critic", 63 | dependencies: [ 64 | .target(name: "Crawler"), 65 | "Decipher", 66 | "Models", 67 | ] 68 | ), 69 | .target( 70 | name: "Editor", 71 | dependencies: [ 72 | .target(name: "Crawler"), 73 | "Decipher", 74 | ] 75 | ), 76 | .target( 77 | name: "Decipher", 78 | dependencies: ["Models"] 79 | ), 80 | .target( 81 | name: "Models", 82 | dependencies: [] 83 | ), 84 | .target( 85 | name: "Informant", 86 | dependencies: [ 87 | "Chalk", 88 | "Critic", 89 | "Pathos", 90 | ] 91 | ), 92 | // workaround for https://bugs.swift.org/browse/SR-15802 93 | // delete it in favor of `DrStringCore` once the issue gets resolved. 94 | .target( 95 | name: "_DrStringCore", 96 | dependencies: [ 97 | "Critic", 98 | "Decipher", 99 | "Editor", 100 | "Informant", 101 | "IsTTY", 102 | "Models", 103 | "Pathos", 104 | "TOMLDecoder", 105 | ] 106 | ), 107 | .target( 108 | name: "DrStringCore", 109 | dependencies: [ 110 | "Critic", 111 | "Decipher", 112 | "Editor", 113 | "Informant", 114 | "IsTTY", 115 | "Models", 116 | "Pathos", 117 | "TOMLDecoder", 118 | ] 119 | ), 120 | .target( 121 | name: "DrStringCLI", 122 | dependencies: [ 123 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 124 | "_DrStringCore" 125 | ] 126 | ), 127 | .executableTarget( 128 | name: "DrString", 129 | dependencies: [ 130 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 131 | "DrStringCLI" 132 | ] 133 | ), 134 | .testTarget( 135 | name: "ModelsTests", 136 | dependencies: ["Models"] 137 | ), 138 | .testTarget( 139 | name: "CriticTests", 140 | dependencies: [ 141 | "Critic", 142 | "Models", 143 | ] 144 | ), 145 | .testTarget( 146 | name: "EditorTests", 147 | dependencies: [ 148 | "Editor", 149 | "Models", 150 | ] 151 | ), 152 | .testTarget( 153 | name: "DecipherTests", 154 | dependencies: [ 155 | "Decipher", 156 | "Models", 157 | ] 158 | ), 159 | .testTarget( 160 | name: "CLITests", 161 | dependencies: [ 162 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 163 | "DrStringCLI", 164 | ], 165 | exclude: ["Fixtures/"] 166 | ), 167 | .testTarget( 168 | name: "DrStringCoreTests", 169 | dependencies: [ 170 | "DrStringCore", 171 | "FileCheck", 172 | "Models", 173 | "Pathos", 174 | ], 175 | exclude: ["Fixtures/"] 176 | ), 177 | ] 178 | ) 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Circular Logo](Documentation/Assets/Logo-Circular-Header.png) 2 | 3 | # Dr. String in the Multiverse of Pedantry 4 | 5 | … or "DrString", for short. 6 | 7 | DrString helps you take control of docstrings in your Swift codebase by finding 8 | and fixing inconsistencies among them. 9 | 10 | ![Demo](Documentation/Assets/Demo.png) 11 | ![Xcode Demo](Documentation/Assets/Demo-Xcode.png) 12 | 13 | ## Documentation 14 | 15 | - [Getting Started][] guides you through how to use DrString in your Swift 16 | project. 17 | - [Overview][] provides the _why_ s and _what_ s of docstring linting. 18 | - [Configuration][] is a reference to all options for behavior customization. 19 | - [Editor Integrations][] exist to improve your user experience with Xcode, and 20 | more! 21 | 22 | [Getting Started]: Documentation/GettingStarted.md 23 | [Overview]: Documentation/Overview.md 24 | [Configuration]: Documentation/Configuration.md 25 | [Editor Integrations]: Documentation/EditorIntegrations.md 26 | 27 | ## License 28 | 29 | [MIT](LICENSE.md). 30 | -------------------------------------------------------------------------------- /Scripts/Dockerfile-5.9.2-focal: -------------------------------------------------------------------------------- 1 | ARG BUILDER_IMAGE=swift:5.9.2-focal 2 | ARG RUNTIME_IMAGE=ubuntu:focal 3 | 4 | # builder image 5 | FROM ${BUILDER_IMAGE} AS builder 6 | RUN apt-get update && apt-get install -y make && rm -r /var/lib/apt/lists/* 7 | WORKDIR /workdir/ 8 | 9 | COPY Sources Sources/ 10 | COPY Tests Tests/ 11 | COPY Scripts/locateswift.sh Scripts/locateswift.sh 12 | COPY Scripts/completions/bash/drstring-completion.bash Scripts/completions/bash/drstring-completion.bash 13 | COPY Scripts/completions/zsh/_drstring Scripts/completions/zsh/_drstring 14 | COPY Scripts/completions/drstring.fish Scripts/completions/drstring.fish 15 | COPY Makefile Makefile 16 | COPY Package.* ./ 17 | 18 | RUN make build 19 | RUN mkdir -p /executables 20 | RUN mkdir -p /completions 21 | RUN install -v .build/release/drstring /executables/ 22 | RUN install -v Scripts/completions/bash/drstring-completion.bash /completions/drstring-completion.bash 23 | RUN install -v Scripts/completions/zsh/_drstring /completions/_drstring 24 | RUN install -v Scripts/completions/drstring.fish /completions/drstring.fish 25 | 26 | # runtime image 27 | FROM ${RUNTIME_IMAGE} 28 | LABEL org.opencontainers.image.source https://github.com/dduan/DrString 29 | COPY --from=builder /executables/drstring /usr/bin 30 | COPY --from=builder /completions/drstring-completion.bash /etc/bash_completion.d/drstring 31 | COPY --from=builder /completions/_drstring /usr/share/zsh/vendor-completions/_drstring 32 | COPY --from=builder /completions/drstring.fish /usr/share/fish/completions/drstring.fish 33 | 34 | 35 | RUN drstring --version 36 | 37 | CMD ["drstring", "check", "-i", "./**/*.swift"] 38 | -------------------------------------------------------------------------------- /Scripts/check-version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | import sys 5 | 6 | expected = re.search(r'let version = "(.+)"', open('Sources/DrStringCore/version.swift').read(), re.M).group(1) 7 | versions = {} 8 | versions['CHANGELOG.md'] = re.search(r'\n##\s*(.+)', open('CHANGELOG.md').read(), re.M).group(1) 9 | 10 | for file in versions: 11 | if expected != versions[file]: 12 | print(f"version mismatch: expected {expected}; found {versions[file]} in {file}", file=sys.stderr) 13 | exit(1) 14 | -------------------------------------------------------------------------------- /Scripts/completions/bash/drstring-completion.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | _drstring() { 4 | cur="${COMP_WORDS[COMP_CWORD]}" 5 | prev="${COMP_WORDS[COMP_CWORD-1]}" 6 | COMPREPLY=() 7 | opts="--version -h --help check explain format extract help" 8 | if [[ $COMP_CWORD == "1" ]]; then 9 | COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) 10 | return 11 | fi 12 | case ${COMP_WORDS[1]} in 13 | (check) 14 | _drstring_check 2 15 | return 16 | ;; 17 | (explain) 18 | _drstring_explain 2 19 | return 20 | ;; 21 | (format) 22 | _drstring_format 2 23 | return 24 | ;; 25 | (extract) 26 | _drstring_extract 2 27 | return 28 | ;; 29 | (help) 30 | _drstring_help 2 31 | return 32 | ;; 33 | esac 34 | COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) 35 | } 36 | _drstring_check() { 37 | opts="--config-file -i --include -x --exclude --no-exclude --ignore-throws --no-ignore-throws --ignore-returns --no-ignore-returns --first-letter --needs-separation --no-needs-separation --vertical-align --no-vertical-align --parameter-style --align-after-colon --no-align-after-colon --format --superfluous-exclusion --no-superfluous-exclusion --empty-patterns --no-empty-patterns --version -h --help" 38 | if [[ $COMP_CWORD == "$1" ]]; then 39 | COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) 40 | return 41 | fi 42 | case $prev in 43 | --config-file) 44 | 45 | return 46 | ;; 47 | -i|--include) 48 | 49 | return 50 | ;; 51 | -x|--exclude) 52 | 53 | return 54 | ;; 55 | --first-letter) 56 | 57 | return 58 | ;; 59 | --needs-separation) 60 | COMPREPLY=( $(compgen -W "description parameters throws returns" -- "$cur") ) 61 | return 62 | ;; 63 | --parameter-style) 64 | 65 | return 66 | ;; 67 | --align-after-colon) 68 | COMPREPLY=( $(compgen -W "description parameters throws returns" -- "$cur") ) 69 | return 70 | ;; 71 | --format) 72 | 73 | return 74 | ;; 75 | esac 76 | COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) 77 | } 78 | _drstring_explain() { 79 | opts="--version -h --help" 80 | if [[ $COMP_CWORD == "$1" ]]; then 81 | COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) 82 | return 83 | fi 84 | COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) 85 | } 86 | _drstring_format() { 87 | opts="--config-file -i --include -x --exclude --no-exclude --ignore-throws --no-ignore-throws --ignore-returns --no-ignore-returns --first-letter --needs-separation --no-needs-separation --vertical-align --no-vertical-align --parameter-style --align-after-colon --no-align-after-colon --column-limit --add-placeholder --no-add-placeholder --start-line --end-line --version -h --help" 88 | if [[ $COMP_CWORD == "$1" ]]; then 89 | COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) 90 | return 91 | fi 92 | case $prev in 93 | --config-file) 94 | 95 | return 96 | ;; 97 | -i|--include) 98 | 99 | return 100 | ;; 101 | -x|--exclude) 102 | 103 | return 104 | ;; 105 | --first-letter) 106 | 107 | return 108 | ;; 109 | --needs-separation) 110 | COMPREPLY=( $(compgen -W "description parameters throws returns" -- "$cur") ) 111 | return 112 | ;; 113 | --parameter-style) 114 | 115 | return 116 | ;; 117 | --align-after-colon) 118 | COMPREPLY=( $(compgen -W "description parameters throws returns" -- "$cur") ) 119 | return 120 | ;; 121 | --column-limit) 122 | 123 | return 124 | ;; 125 | --start-line) 126 | 127 | return 128 | ;; 129 | --end-line) 130 | 131 | return 132 | ;; 133 | esac 134 | COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) 135 | } 136 | _drstring_extract() { 137 | opts="--config-file -i --include -x --exclude --no-exclude --version -h --help" 138 | if [[ $COMP_CWORD == "$1" ]]; then 139 | COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) 140 | return 141 | fi 142 | case $prev in 143 | --config-file) 144 | 145 | return 146 | ;; 147 | -i|--include) 148 | 149 | return 150 | ;; 151 | -x|--exclude) 152 | 153 | return 154 | ;; 155 | esac 156 | COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) 157 | } 158 | _drstring_help() { 159 | opts="--version" 160 | if [[ $COMP_CWORD == "$1" ]]; then 161 | COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) 162 | return 163 | fi 164 | COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) 165 | } 166 | 167 | 168 | complete -F _drstring drstring 169 | -------------------------------------------------------------------------------- /Scripts/generateexplainers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from glob import glob 5 | from os import path 6 | 7 | if len(sys.argv) != 2: 8 | print(f"Usage: {sys.argv[0]} PATH_TO_EXPLAINER_MARKDOWN_DIRECTORY") 9 | exit(1) 10 | 11 | line_template = ''' 12 | "{id}": .init( 13 | id: "{id}", 14 | summary: """ 15 | {summary} 16 | """, 17 | rightExample: """ 18 | {good_example} 19 | """, 20 | wrongExample: """ 21 | {bad_example} 22 | """ 23 | ), 24 | ''' 25 | 26 | line_template_no_example = ''' 27 | "{id}": .init( 28 | id: "{id}", 29 | summary: """ 30 | {summary} 31 | """, 32 | rightExample: nil, 33 | wrongExample: nil 34 | ), 35 | ''' 36 | pad = " " 37 | 38 | 39 | def pad_lines(text): 40 | lines = [] 41 | for line in text.split('\n'): 42 | lines.append(pad + line) 43 | return '\n'.join(lines) 44 | 45 | 46 | lines = [] 47 | paths = glob(path.join(sys.argv[1], 'E*.md')) 48 | paths.sort() 49 | for exp_path in paths: 50 | exp_id = path.split(exp_path)[1].split('.')[0] 51 | with open(exp_path) as exp_file: 52 | sections = exp_file.read().split("\n\n\n") 53 | title = sections[0][3:] 54 | summary = pad_lines(sections[1].strip()) 55 | 56 | bad_example = None 57 | good_example = None 58 | if len(sections) > 2: 59 | examples_content = sections[2].split("```") 60 | bad_example = pad_lines(examples_content[1][5:].strip()) 61 | good_example = pad_lines(examples_content[3][5:].strip()) 62 | if bad_example and good_example: 63 | lines.append( 64 | line_template.format( 65 | id=exp_id, 66 | summary=summary, 67 | bad_example=bad_example, 68 | good_example=good_example)) 69 | else: 70 | lines.append( 71 | line_template_no_example.format( 72 | id=exp_id, 73 | summary=summary)) 74 | 75 | 76 | body = ''.join(lines) 77 | header = """// DO NOT EDIT: this is generated by Scripts/generateexplainers.py 78 | 79 | extension Explainer { 80 | public static var all: [String: Explainer] { 81 | [ 82 | """ 83 | footer = """] 84 | } 85 | } 86 | """ 87 | 88 | print(''.join([header, body, footer])) 89 | -------------------------------------------------------------------------------- /Scripts/locateswift.sh: -------------------------------------------------------------------------------- 1 | echo $(swift -print-target-info | grep runtimeResourcePath | cut -f 2 -d ':' | cut -f 2 -d '"') 2 | -------------------------------------------------------------------------------- /Scripts/package-darwin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | BUILD_PATH=.build/apple/Products/Release 6 | TEMP=$(mktemp -d) 7 | COMPLETIONS_PATH=$TEMP/completions 8 | ARCHIVE=drstring_darwin.tar.gz 9 | rm -rf $COMPLETIONS_PATH 10 | mkdir -p $COMPLETIONS_PATH 11 | cp -r Scripts/completions/* $COMPLETIONS_PATH 12 | cp $BUILD_PATH/drstring $TEMP/ 13 | tar -C $TEMP -czf $ARCHIVE drstring completions 14 | -------------------------------------------------------------------------------- /Scripts/ubuntu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! command -v docker > /dev/null; then 4 | echo "Install docker https://docker.com" >&2 5 | exit 1 6 | fi 7 | action=$1 8 | swift=$2 9 | ubuntu=$3 10 | arch=$4 11 | dockerfile=$(mktemp) 12 | echo "FROM swift:$swift-$ubuntu" > $dockerfile 13 | echo 'ADD . DrString' >> $dockerfile 14 | echo 'WORKDIR DrString' >> $dockerfile 15 | echo 'RUN apt-get update && apt-get install -y make' >> $dockerfile 16 | echo "RUN make $action" >> $dockerfile 17 | image=drstring 18 | docker image rm -f "$image:$image" || true > /dev/null 19 | docker build --platform=linux/$arch -t "$image:$image" -f $dockerfile . 20 | -------------------------------------------------------------------------------- /Scripts/ubuntuarchive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | SWIFT_VERSION=$1 5 | UBUNTU_RELEASE=$2 6 | DOCKER_ARCH=$3 7 | IMAGE_TAG="${SWIFT_VERSION}-${UBUNTU_RELEASE}-${DOCKER_ARCH}" 8 | ARCHIVE=drstring-ubuntu-${UBUNTU_RELEASE}.tar.gz 9 | 10 | docker build --platform linux/$DOCKER_ARCH --force-rm -f "Scripts/Dockerfile-${SWIFT_VERSION}-${UBUNTU_RELEASE}" --tag $IMAGE_TAG . 11 | 12 | TEMP=$(mktemp -d) 13 | EXES=(/usr/bin/drstring) 14 | ALL=( "${EXES[@]}" /etc/bash_completion.d/drstring /usr/share/zsh/vendor-completions/_drstring /usr/share/fish/completions/drstring.fish ) 15 | for content in ${ALL[@]}; do 16 | mkdir -p "$TEMP/$(dirname $content)" 17 | docker run $IMAGE_TAG cat $content > "$TEMP/$content" 18 | done 19 | 20 | for exe in ${EXES[@]}; do 21 | chmod +x "$TEMP/$exe" 22 | done 23 | 24 | tar -C $TEMP -czf $ARCHIVE etc usr 25 | -------------------------------------------------------------------------------- /Sources/Crawler/DocExtractor.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | import Pathos 3 | import SwiftParser 4 | import SwiftSyntax 5 | 6 | public func extractDocs(fromSource sourcePath: Path) throws -> ([Documentable], String) { 7 | let source = try sourcePath.readUTF8String() 8 | let extractor = DocExtractor(sourceText: source, sourcePath: sourcePath) 9 | return (try extractor.extractDocs(), source) 10 | } 11 | 12 | public func extractDocs(fromSource source: String, sourcePath: Path?) throws -> [Documentable] { 13 | let extractor = DocExtractor(sourceText: source, sourcePath: sourcePath) 14 | return try extractor.extractDocs() 15 | } 16 | 17 | final class DocExtractor: SyntaxRewriter { 18 | private var findings: [Documentable] = [] 19 | private let syntax: SourceFileSyntax 20 | private let converter: SourceLocationConverter 21 | 22 | init(sourceText: String, sourcePath: Path?) { 23 | let tree = Parser.parse(source: sourceText) 24 | self.syntax = tree 25 | self.converter = SourceLocationConverter(fileName: sourcePath.map(String.init(describing:)) ?? "", tree: tree) 26 | } 27 | 28 | func extractDocs() throws -> [Documentable] { 29 | _ = self.visit(syntax) 30 | return self.findings 31 | } 32 | 33 | override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax { 34 | let location = node.startLocation(converter: self.converter) 35 | let endLocation = node.endLocation(converter: self.converter) 36 | let parameters = node.parameters 37 | let signatureText = node.name.description 38 | + "(\(parameters.reduce("") { $0 + ($1.label ?? $1.name) + ":" }))" 39 | let finding = Documentable( 40 | path: location.file, 41 | startLine: location.line - 1, 42 | startColumn: location.column - 1, 43 | endLine: endLocation.line - 1, 44 | name: signatureText, 45 | docLines: node.leadingTrivia.docStringLines, 46 | children: [], 47 | details: Documentable.Details( 48 | throws: node.throws, 49 | returnType: node.returnType, 50 | parameters: parameters) 51 | ) 52 | self.findings.append(finding) 53 | return DeclSyntax(node) 54 | } 55 | 56 | override func visit(_ node: InitializerDeclSyntax) -> DeclSyntax { 57 | let location = node.startLocation(converter: self.converter) 58 | let endLocation = node.endLocation(converter: self.converter) 59 | let parameters = node.signature.parameterClause.parameters.map { $0.parameter } 60 | let signatureText = "init(\(parameters.reduce("") { $0 + ($1.label ?? $1.name) + ":" }))" 61 | let finding = Documentable( 62 | path: location.file, 63 | startLine: location.line - 1, 64 | startColumn: location.column - 1, 65 | endLine: endLocation.line - 1, 66 | name: signatureText, 67 | docLines: node.leadingTrivia.docStringLines, 68 | children: [], 69 | details: .init( 70 | throws: node.throws, 71 | returnType: nil, 72 | parameters: parameters) 73 | ) 74 | self.findings.append(finding) 75 | return DeclSyntax(node) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/Crawler/SwiftSyntax+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | import SwiftSyntax 3 | 4 | extension FunctionParameterSyntax { 5 | var parameter: Parameter { 6 | let label = self.firstName.text 7 | let name = self.secondName?.text ?? self.firstName.text 8 | let type = self.type.description 9 | let hasDefault = self.defaultValue != nil 10 | let isVariadic = self.ellipsis?.text == "..." 11 | return Parameter(label: label, name: name, type: type, isVariadic: isVariadic, hasDefault: hasDefault) 12 | } 13 | } 14 | 15 | extension FunctionDeclSyntax { 16 | var `throws`: Bool { 17 | return self.signature.effectSpecifiers?.throwsSpecifier != nil 18 | } 19 | 20 | var returnType: String? { 21 | let trailingTrivaLength = self.signature.returnClause?.type.trailingTriviaLength.utf8Length ?? 0 22 | return (self.signature.returnClause?.type.description.utf8.dropLast(trailingTrivaLength)) 23 | .flatMap(String.init) 24 | } 25 | 26 | var parameters: [Parameter] { 27 | return self.signature.parameterClause.parameters.children(viewMode: .sourceAccurate).compactMap { syntax in 28 | return FunctionParameterSyntax(syntax)?.parameter 29 | } 30 | } 31 | } 32 | 33 | extension InitializerDeclSyntax { 34 | var `throws`: Bool { 35 | return self.signature.effectSpecifiers?.throwsSpecifier != nil 36 | } 37 | } 38 | 39 | extension Trivia { 40 | var docStringLines: [String] { 41 | var result = [String]() 42 | outer: for piece in self.reversed() { 43 | switch piece { 44 | case .spaces, .tabs, .verticalTabs: 45 | continue outer 46 | case .newlines(let n): 47 | if n > 1 { 48 | break outer 49 | } 50 | case .carriageReturns(let n): 51 | if n > 1 { 52 | break outer 53 | } 54 | case .carriageReturnLineFeeds(let n): 55 | if n > 1 { 56 | break outer 57 | } 58 | case .docLineComment(let s): 59 | result.append(s) 60 | default: 61 | break outer 62 | } 63 | } 64 | 65 | return result.reversed() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/Critic/DocProblem.swift: -------------------------------------------------------------------------------- 1 | import Crawler 2 | 3 | public struct DocProblem { 4 | public let docName: String 5 | public let filePath: String 6 | public let line: Int 7 | public let column: Int 8 | public let detail: Detail 9 | 10 | public init(docName: String, filePath: String, line: Int, column: Int, detail: Detail) { 11 | self.docName = docName 12 | self.filePath = filePath 13 | self.line = line 14 | self.column = column 15 | self.detail = detail 16 | } 17 | 18 | public enum Detail { 19 | case redundantParameter(String) 20 | case missingParameter(String, String) 21 | case missingThrow 22 | case missingReturn(String) 23 | case preDashSpaceInParameter(Int, String, String) // expected, actual, name 24 | case spaceBetweenDashAndParameterKeyword(String, String, String) // Actual, keyword, name 25 | case spaceBeforeParameterName(String, String, String) // Actual, keyword, name 26 | case spaceBeforeColon(String, String) // Actual, name 27 | case preDashSpace(String, String) // Keyword, actual 28 | case spaceBetweenDashAndKeyword(String, String) // Keyword, actual 29 | case verticalAlignment(Int, String, Int) // Expected, name/keyword, line number 30 | case spaceAfterColon(String, String) // Keyword, actual 31 | case keywordCasingForParameter(String, String, String) // Actual, expected, name 32 | case keywordSpelling(String, String) // Actual, expected 33 | case descriptionShouldEndWithEmptyLine 34 | case sectionShouldEndWithEmptyLine(String) // keyword or parameter name 35 | case redundantKeyword(String) // keyword 36 | case redundantTextFollowingParameterHeader(String) // keyword 37 | case excludedYetNoProblemIsFound(String?) // config path 38 | case excludedYetNotIncluded 39 | case parametersAreNotGrouped 40 | case parametersAreNotSeparated 41 | case missingColon(String) // parameter name/keyword 42 | case invalidPattern(String, String?) // adjective (e.g. "included"), config path 43 | 44 | public var explainerID: String { 45 | switch self { 46 | case .redundantParameter: 47 | return "E001" 48 | case .missingParameter: 49 | return "E002" 50 | case .missingThrow: 51 | return "E003" 52 | case .missingReturn: 53 | return "E004" 54 | case .preDashSpaceInParameter, .preDashSpace: 55 | return "E005" 56 | case .spaceBetweenDashAndParameterKeyword, .spaceBetweenDashAndKeyword: 57 | return "E006" 58 | case .spaceBeforeParameterName: 59 | return "E007" 60 | case .spaceBeforeColon: 61 | return "E008" 62 | case .verticalAlignment: 63 | return "E009" 64 | case .spaceAfterColon: 65 | return "E010" 66 | case .keywordCasingForParameter, .keywordSpelling: 67 | return "E011" 68 | case .descriptionShouldEndWithEmptyLine, .sectionShouldEndWithEmptyLine: 69 | return "E012" 70 | case .redundantKeyword: 71 | return "E013" 72 | case .redundantTextFollowingParameterHeader: 73 | return "E014" 74 | case .excludedYetNoProblemIsFound: 75 | return "E015" 76 | case .excludedYetNotIncluded: 77 | return "E016" 78 | case .parametersAreNotGrouped: 79 | return "E017" 80 | case .parametersAreNotSeparated: 81 | return "E018" 82 | case .missingColon: 83 | return "E019" 84 | case .invalidPattern: 85 | return "E020" 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/Critic/Explainer.swift: -------------------------------------------------------------------------------- 1 | public struct Explainer { 2 | public let id: String 3 | public let summary: String 4 | public let rightExample: String? 5 | public let wrongExample: String? 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Decipher/Parsing.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | 3 | enum Parsing { 4 | enum State { 5 | case start 6 | case description 7 | case groupedParameterStart 8 | case groupedParameter 9 | case separateParameter 10 | case `returns` 11 | case `throws` 12 | } 13 | 14 | enum LineResult: Equatable { 15 | case words(TextLeadByWhitespace) 16 | case groupedParametersHeader(String, TextLeadByWhitespace, String, Bool, TextLeadByWhitespace) 17 | case groupedParameter(String, TextLeadByWhitespace, String, Bool, TextLeadByWhitespace) // name, description, raw text 18 | case parameter(String, TextLeadByWhitespace, TextLeadByWhitespace, String, Bool, TextLeadByWhitespace) 19 | case `returns`(String, TextLeadByWhitespace, String, Bool, TextLeadByWhitespace) 20 | case `throws`(String, TextLeadByWhitespace, String, Bool, TextLeadByWhitespace) 21 | } 22 | 23 | enum LineError: Error { 24 | case missingCommentHead(String) 25 | } 26 | 27 | enum StructuralError: Error { 28 | case invalidStart 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/DrString/main.swift: -------------------------------------------------------------------------------- 1 | import DrStringCLI 2 | 3 | run(arguments: Array(CommandLine.arguments.dropFirst())) 4 | -------------------------------------------------------------------------------- /Sources/DrStringCLI/ConfigFileError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum ConfigFileError: Error, LocalizedError { 4 | case configFileDoesNotExist(String) 5 | case configFileIsInvalid(path: String, underlyingError: Error) 6 | 7 | var errorDescription: String? { 8 | switch self { 9 | case .configFileDoesNotExist(let path): 10 | return "Could not find configuration file '\(path)'." 11 | case .configFileIsInvalid(let path, let error): 12 | return "File '\(path)' doesn't contain valid configuration for DrString. Underlying error: \(error)" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/DrStringCLI/overrides.swift: -------------------------------------------------------------------------------- 1 | import _DrStringCore 2 | 3 | extension Configuration { 4 | mutating func extend(with options: SharedCommandLineOptions) { 5 | self.extend(with: options.basics) 6 | self.ignoreDocstringForThrows = options.ignoreThrows ?? self.ignoreDocstringForThrows 7 | self.ignoreDocstringForReturns = options.ignoreReturns ?? self.ignoreDocstringForReturns 8 | self.verticalAlignParameterDescription = options.verticalAlign ?? self.verticalAlignParameterDescription 9 | self.firstKeywordLetter = options.firstLetter ?? self.firstKeywordLetter 10 | 11 | if !options.needsSeparation.isEmpty { 12 | self.separatedSections = options.needsSeparation 13 | } 14 | 15 | if options.noNeedsSeparation { 16 | self.separatedSections = [] 17 | } 18 | 19 | if !options.alignAfterColon.isEmpty { 20 | self.alignAfterColon = options.alignAfterColon 21 | } 22 | 23 | if options.noAlignAfterColon { 24 | self.alignAfterColon = [] 25 | } 26 | } 27 | 28 | mutating func extend(with basicOptions: SharedCommandLineBasicOptions) { 29 | if !basicOptions.include.isEmpty { 30 | self.includedPaths = basicOptions.include 31 | } 32 | 33 | if !basicOptions.exclude.isEmpty { 34 | self.excludedPaths = basicOptions.exclude 35 | } 36 | 37 | if basicOptions.noExclude { 38 | self.excludedPaths = [] 39 | } 40 | } 41 | } 42 | 43 | 44 | extension Configuration { 45 | mutating func extend(with checkCommand: Check) { 46 | self.extend(with: checkCommand.options) 47 | self.outputFormat = checkCommand.format ?? self.outputFormat 48 | self.allowSuperfluousExclusion = checkCommand.superfluousExclusion ?? self.allowSuperfluousExclusion 49 | self.allowEmptyPatterns = checkCommand.emptyPatterns ?? self.allowEmptyPatterns 50 | } 51 | } 52 | 53 | 54 | extension Configuration { 55 | mutating func extend(with formatCommand: Format) { 56 | self.extend(with: formatCommand.options) 57 | self.columnLimit = formatCommand.columnLimit ?? self.columnLimit 58 | self.addPlaceholder = formatCommand.addPlaceholder ?? self.addPlaceholder 59 | self.startLine = formatCommand.startLine ?? self.startLine 60 | self.endLine = formatCommand.endLine ?? self.endLine 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/DrStringCLI/run.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import Pathos 3 | import TOMLDecoder 4 | import Models 5 | import _DrStringCore 6 | 7 | private func configFromFile(_ configPath: Path) throws -> Configuration? { 8 | if let configText = try? configPath.readUTF8String() { 9 | do { 10 | let decoded = try TOMLDecoder().decode(Configuration.self, from: configText) 11 | return decoded 12 | } catch let error { 13 | throw ConfigFileError.configFileIsInvalid(path: String(describing: configPath), underlyingError: error) 14 | } 15 | } 16 | 17 | return nil 18 | } 19 | 20 | private func makeConfig(from basicOptions: SharedCommandLineBasicOptions) throws -> (String?, Configuration) { 21 | let explicitPath = basicOptions.configFile.map(Path.init) 22 | if let explicitPath = explicitPath, 23 | (try? explicitPath.metadata().fileType.isFile) != .some(true) 24 | { 25 | throw ConfigFileError.configFileDoesNotExist(String(describing: explicitPath)) 26 | } 27 | 28 | let exampleInput = basicOptions.include.first { !$0.contains("*") } 29 | let configPath = explicitPath ?? seekConfigFile(for: Path(exampleInput ?? ".")) 30 | var config = Configuration() 31 | var configPathResult: String? 32 | if let decoded = try configFromFile(configPath) { 33 | config = decoded 34 | configPathResult = String(describing: configPath) 35 | } 36 | 37 | return (configPathResult, config) 38 | } 39 | 40 | private let kDefaultConfigurationPath = Path(".drstring.toml") 41 | private func seekConfigFile(for path: Path) -> Path { 42 | guard let dir = try? path.absolute() else { 43 | return kDefaultConfigurationPath 44 | } 45 | 46 | for parent in dir.parents { 47 | let path = parent + kDefaultConfigurationPath 48 | if (try? path.metadata().fileType.isFile) == .some(true) { 49 | return path 50 | } 51 | } 52 | 53 | return kDefaultConfigurationPath 54 | } 55 | 56 | extension Command { 57 | init?(command: ParsableCommand) throws { 58 | switch command { 59 | case let command as Check: 60 | var (configPath, config) = try makeConfig(from: command.options.basics) 61 | config.extend(with: command) 62 | self = .check(configFile: configPath, config: config) 63 | case let command as Format: 64 | var (_, config) = try makeConfig(from: command.options.basics) 65 | config.extend(with: command) 66 | self = .format(config) 67 | case let command as Explain: 68 | self = .explain(command.problemID) 69 | case let command as Extract: 70 | var (_, config) = try makeConfig(from: command.basics) 71 | config.extend(with: command.basics) 72 | self = .extract(config) 73 | default: 74 | return nil 75 | } 76 | } 77 | } 78 | 79 | public func run(arguments: [String]) { 80 | do { 81 | var parsedCommand = try Main.parseAsRoot(arguments) 82 | if let command = try Command(command: parsedCommand) { 83 | try execute(command) 84 | } else { 85 | try parsedCommand.run() 86 | } 87 | } catch let error { 88 | Main.exit(withError: error) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/DrStringCore/Command.swift: -------------------------------------------------------------------------------- 1 | public enum Command { 2 | case check(configFile: String?, config: Configuration) 3 | case format(Configuration) 4 | case explain([String]) 5 | case extract(Configuration) 6 | 7 | var config: Configuration? { 8 | switch self { 9 | case .check(_, let config): 10 | return config 11 | case .format(let config): 12 | return config 13 | case .explain: 14 | return nil 15 | case .extract(let config): 16 | return config 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/DrStringCore/Configuration.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | 3 | public struct Configuration: Codable { 4 | public var includedPaths: [String] = [] 5 | public var excludedPaths: [String] = [] 6 | public var ignoreDocstringForThrows: Bool = false 7 | public var ignoreDocstringForReturns: Bool = false 8 | public var verticalAlignParameterDescription: Bool = false 9 | public var allowSuperfluousExclusion: Bool = false 10 | public var allowEmptyPatterns: Bool = false 11 | public var firstKeywordLetter: FirstKeywordLetterCasing = .uppercase 12 | public var outputFormat: OutputFormat = .automatic 13 | public var separatedSections: [Section] = [] 14 | public var parameterStyle: ParameterStyle = .whatever 15 | public var alignAfterColon: [Section] = [] 16 | public var columnLimit: Int? 17 | public var addPlaceholder: Bool = false 18 | public var startLine: Int? 19 | public var endLine: Int? 20 | 21 | public init() {} 22 | public init(from decoder: Decoder) throws { 23 | let values = try decoder.container(keyedBy: CodingKeys.self) 24 | var config = Configuration() 25 | 26 | config.includedPaths = try values.decode([String].self, forKey: .include) 27 | config.columnLimit = try values.decodeIfPresent(Int.self, forKey: .columnLimit) 28 | 29 | if let excludedPaths = try values.decodeIfPresent([String].self, forKey: .exclude) { 30 | config.excludedPaths = excludedPaths 31 | } 32 | 33 | if let ignoreDocstringForThrows = try values.decodeIfPresent(Bool.self, forKey: .ignoreThrows) { 34 | config.ignoreDocstringForThrows = ignoreDocstringForThrows 35 | } 36 | 37 | if let ignoreDocstringForReturns = try values.decodeIfPresent(Bool.self, forKey: .ignoreReturns) { 38 | config.ignoreDocstringForReturns = ignoreDocstringForReturns 39 | } 40 | 41 | if let verticalAlignParameterDescription = try values.decodeIfPresent(Bool.self, forKey: .verticalAlign) { 42 | config.verticalAlignParameterDescription = verticalAlignParameterDescription 43 | } 44 | 45 | if let allowSuperfluousExclusion = try values.decodeIfPresent(Bool.self, forKey: .superfluousExclusion) { 46 | config.allowSuperfluousExclusion = allowSuperfluousExclusion 47 | } 48 | 49 | if let allowEmptyPatterns = try values.decodeIfPresent(Bool.self, forKey: .emptyPatterns) { 50 | config.allowEmptyPatterns = allowEmptyPatterns 51 | } 52 | 53 | if let firstKeywordLetter = try values.decodeIfPresent(FirstKeywordLetterCasing.self, forKey: .firstKeywordLetter) { 54 | config.firstKeywordLetter = firstKeywordLetter 55 | } 56 | 57 | if let outputFormat = try values.decodeIfPresent(OutputFormat.self, forKey: .format) { 58 | config.outputFormat = outputFormat 59 | } 60 | 61 | if let separatedSections = try values.decodeIfPresent([Section].self, forKey: .separations) { 62 | config.separatedSections = separatedSections 63 | } 64 | 65 | if let parameterStyle = try values.decodeIfPresent(ParameterStyle.self, forKey: .parameterStyle) { 66 | config.parameterStyle = parameterStyle 67 | } 68 | 69 | if let alignAfterColon = try values.decodeIfPresent([Section].self, forKey: .alignAfterColon) { 70 | config.alignAfterColon = alignAfterColon 71 | } 72 | 73 | if let addPlaceholder = try values.decodeIfPresent(Bool.self, forKey: .addPlaceholder) { 74 | config.addPlaceholder = addPlaceholder 75 | } 76 | 77 | if let startLine = try values.decodeIfPresent(Int.self, forKey: .startLine) { 78 | config.startLine = startLine 79 | } 80 | 81 | if let endLine = try values.decodeIfPresent(Int.self, forKey: .endLine) { 82 | config.endLine = endLine 83 | } 84 | 85 | self = config 86 | } 87 | 88 | public func encode(to encoder: Encoder) throws { 89 | var values = encoder.container(keyedBy: CodingKeys.self) 90 | try values.encodeIfPresent(self.endLine, forKey: .endLine) 91 | try values.encodeIfPresent(self.startLine, forKey: .startLine) 92 | try values.encodeIfPresent(self.columnLimit, forKey: .columnLimit) 93 | try values.encode(self.addPlaceholder, forKey: .addPlaceholder) 94 | try values.encode(self.alignAfterColon, forKey: .alignAfterColon) 95 | try values.encode(self.parameterStyle, forKey: .parameterStyle) 96 | try values.encode(self.separatedSections, forKey: .separations) 97 | try values.encode(self.outputFormat, forKey: .format) 98 | try values.encode(self.firstKeywordLetter, forKey: .firstKeywordLetter) 99 | try values.encode(self.allowSuperfluousExclusion, forKey: .superfluousExclusion) 100 | try values.encode(self.allowEmptyPatterns, forKey: .emptyPatterns) 101 | try values.encode(self.verticalAlignParameterDescription, forKey: .verticalAlign) 102 | try values.encode(self.ignoreDocstringForReturns, forKey: .ignoreReturns) 103 | try values.encode(self.ignoreDocstringForThrows, forKey: .ignoreThrows) 104 | try values.encode(self.excludedPaths, forKey: .exclude) 105 | try values.encode(self.includedPaths, forKey: .include) 106 | } 107 | 108 | 109 | enum CodingKeys: String, CodingKey { 110 | case include = "include" 111 | case exclude = "exclude" 112 | case ignoreThrows = "ignore-throws" 113 | case ignoreReturns = "ignore-returns" 114 | case verticalAlign = "vertical-align" 115 | case firstKeywordLetter = "first-letter" 116 | case format = "format" 117 | case separations = "needs-separation" 118 | case superfluousExclusion = "superfluous-exclusion" 119 | case emptyPatterns = "empty-patterns" 120 | case parameterStyle = "parameter-style" 121 | case alignAfterColon = "align-after-colon" 122 | case columnLimit = "column-limit" 123 | case addPlaceholder = "add-placeholder" 124 | case startLine = "start-line" 125 | case endLine = "end-line" 126 | } 127 | 128 | public enum OutputFormat: String, Equatable, Codable { 129 | case automatic 130 | case terminal 131 | case plain 132 | case paths 133 | } 134 | 135 | public enum FirstKeywordLetterCasing: String, Equatable, Codable { 136 | case uppercase 137 | case lowercase 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Sources/DrStringCore/Shims.swift: -------------------------------------------------------------------------------- 1 | import Editor 2 | import Models 3 | 4 | public typealias Edit = Editor.Edit 5 | public typealias Section = Models.Section 6 | public typealias ParameterStyle = Models.ParameterStyle 7 | -------------------------------------------------------------------------------- /Sources/DrStringCore/Timing.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Darwin) 2 | import Darwin 3 | #else 4 | import Glibc 5 | #endif 6 | 7 | func getTime() -> timeval { 8 | var time = timeval() 9 | gettimeofday(&time, nil) 10 | return time 11 | } 12 | 13 | func readableDiff(from: timeval, to: timeval) -> String { 14 | var numericDifference = Double(to.tv_sec) + Double(to.tv_usec) / 1_000_000 15 | - (Double(from.tv_sec) + Double(from.tv_usec) / 1_000_000) 16 | var result = [String]() 17 | if numericDifference > 3600 { 18 | let hour = Int(numericDifference / 3600) 19 | result.append("\(hour)h") 20 | numericDifference -= Double(hour * 3600) 21 | } 22 | 23 | if numericDifference > 60 { 24 | let minute = Int(numericDifference / 60) 25 | result.append("\(minute)m") 26 | numericDifference -= Double(minute * 60) 27 | } 28 | 29 | if numericDifference != 0 { 30 | let seconds = Int(floor(numericDifference)) 31 | result.append("\(seconds)s") 32 | numericDifference -= Double(seconds) 33 | } 34 | 35 | if numericDifference != 0 { 36 | let milliseconds = Int(numericDifference * 1000) 37 | result.append("\(milliseconds)ms") 38 | } 39 | 40 | return result.joined(separator: " ") 41 | } 42 | -------------------------------------------------------------------------------- /Sources/DrStringCore/check.swift: -------------------------------------------------------------------------------- 1 | import Crawler 2 | import Critic 3 | import Informant 4 | import IsTTY 5 | import Pathos 6 | 7 | #if canImport(Darwin) 8 | import Darwin 9 | #else 10 | import Glibc 11 | #endif 12 | import Dispatch 13 | import Foundation 14 | 15 | func report(_ problem: DocProblem, format: Configuration.OutputFormat) { 16 | let output: String 17 | switch (format, IsTerminal.standardOutput) { 18 | case (.paths, _): 19 | output = problem.filePath 20 | case (.automatic, true), (.terminal, _): 21 | output = ttyText(for: problem) + "\n" 22 | case (.automatic, false), (.plain, _): 23 | output = plainText(for: problem) + "\n" 24 | } 25 | 26 | print(output) 27 | } 28 | 29 | enum CheckError: Error, LocalizedError { 30 | case missingInput 31 | case foundProblems(String) 32 | 33 | var errorDescription: String? { 34 | switch self { 35 | case .missingInput: 36 | return "Paths to source files are missing. Please provide some." 37 | case .foundProblems(let summary): 38 | return summary 39 | } 40 | } 41 | } 42 | 43 | public func check(with config: Configuration, configFile: String?) throws { 44 | if config.includedPaths.isEmpty { 45 | throw CheckError.missingInput 46 | } 47 | 48 | let startTime = getTime() 49 | var problemCount = 0 50 | var fileCount = 0 51 | let ignoreThrows = config.ignoreDocstringForThrows 52 | let ignoreReturns = config.ignoreDocstringForReturns 53 | let firstLetterUpper: Bool 54 | 55 | switch config.firstKeywordLetter { 56 | case .lowercase: 57 | firstLetterUpper = false 58 | case .uppercase: 59 | firstLetterUpper = true 60 | } 61 | 62 | let group = DispatchGroup() 63 | let queue = DispatchQueue(label: "ca.duan.DrString.concurrent", attributes: .concurrent) 64 | let serialQueue = DispatchQueue(label: "ca.duan.DrString.serial") 65 | 66 | let (included, invalidIncludePatterns) = expandGlob(patterns: config.includedPaths.map(Path.init)) 67 | let (excluded, invalidExcludePatterns) = expandGlob(patterns: config.excludedPaths.map(Path.init)) 68 | 69 | if !config.allowEmptyPatterns { 70 | let allInvalidPatterns = invalidIncludePatterns.map { ($0, "inclusion") } 71 | + invalidExcludePatterns.map { ($0, "exclusion") } 72 | 73 | for (pattern, description) in allInvalidPatterns { 74 | report( 75 | .init( 76 | docName: String(describing: pattern), 77 | filePath: String(describing: pattern), 78 | line: 0, 79 | column: 0, 80 | detail: .invalidPattern(description, configFile)), 81 | format: config.outputFormat 82 | ) 83 | } 84 | 85 | problemCount += allInvalidPatterns.count 86 | } 87 | 88 | for path in included { 89 | let isPathExcluded = excluded.contains(path) 90 | guard !(isPathExcluded && config.allowSuperfluousExclusion) else { 91 | continue 92 | } 93 | 94 | queue.async(group: group) { 95 | do { 96 | var foundProblems = false 97 | let (documentables, _) = try extractDocs(fromSource: path) 98 | for documentable in documentables.compactMap({ $0 }) { 99 | let problems = try documentable.validate( 100 | ignoreThrows: ignoreThrows, 101 | ignoreReturns: ignoreReturns, 102 | firstLetterUpper: firstLetterUpper, 103 | needsSeparation: config.separatedSections, 104 | verticalAlign: config.verticalAlignParameterDescription, 105 | parameterStyle: config.parameterStyle, 106 | alignAfterColon: config.alignAfterColon 107 | ) 108 | if !problems.isEmpty { 109 | foundProblems = true 110 | if !isPathExcluded { 111 | serialQueue.async { 112 | problemCount += problems.count 113 | } 114 | for problem in problems { 115 | report(problem, format: config.outputFormat) 116 | } 117 | } 118 | } 119 | } 120 | 121 | if !foundProblems && 122 | !config.allowSuperfluousExclusion && 123 | config.excludedPaths.contains(String(describing: path)) 124 | { 125 | report( 126 | .init( 127 | docName: "", 128 | filePath: String(describing: path), 129 | line: 0, 130 | column: 0, 131 | detail: .excludedYetNoProblemIsFound(configFile) 132 | ), 133 | format: config.outputFormat 134 | ) 135 | 136 | serialQueue.async { 137 | problemCount += 1 138 | } 139 | } 140 | } catch {} 141 | } 142 | 143 | fileCount += 1 144 | } 145 | 146 | group.wait() 147 | 148 | if !config.allowSuperfluousExclusion { 149 | for path in config.excludedPaths.filter({ !$0.contains("*") && !included.contains(Path($0)) }) { 150 | report( 151 | .init( 152 | docName: "", 153 | filePath: String(describing: path), 154 | line: 0, 155 | column: 0, 156 | detail: .excludedYetNotIncluded 157 | ), 158 | format: config.outputFormat 159 | ) 160 | 161 | serialQueue.async { 162 | problemCount += 1 163 | } 164 | } 165 | } 166 | 167 | let elapsedTime = readableDiff(from: startTime, to: getTime()) 168 | 169 | if problemCount > 0 { 170 | let summary: String 171 | if IsTerminal.standardError { 172 | summary = "Found \(String(problemCount), color: .red) problem\(problemCount > 1 ? "s" : "") in \(String(fileCount), color: .blue) file\(fileCount > 1 ? "s" : "") in \(elapsedTime, color: .blue)\n" 173 | } else { 174 | summary = "Found \(problemCount) problem\(problemCount > 1 ? "s" : "") in \(fileCount) file\(problemCount > 1 ? "s" : "") in \(elapsedTime)\n" 175 | } 176 | 177 | throw CheckError.foundProblems(summary) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Sources/DrStringCore/execute.swift: -------------------------------------------------------------------------------- 1 | public func execute(_ command: Command) throws { 2 | switch command { 3 | case .check(let configFile, let config): 4 | try check(with: config, configFile: configFile) 5 | case .format(let config): 6 | try format(with: config) 7 | case .explain(let problemIDs): 8 | try explain(problemIDs) 9 | case .extract(let config): 10 | try extract(with: config) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/DrStringCore/expandGlob.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | 3 | func expandGlob(patterns: [Path]) -> (Set, Set) { 4 | var valid = Set() 5 | var invalid = Set() 6 | for pattern in patterns { 7 | let expanded = (try? pattern.glob()) ?? [] 8 | if expanded.isEmpty { 9 | invalid.insert(pattern) 10 | } else { 11 | valid.formUnion(expanded) 12 | } 13 | } 14 | 15 | return (valid, invalid) 16 | } 17 | -------------------------------------------------------------------------------- /Sources/DrStringCore/explain.swift: -------------------------------------------------------------------------------- 1 | import Critic 2 | import Informant 3 | 4 | #if canImport(Darwin) 5 | import Darwin 6 | #else 7 | import Glibc 8 | #endif 9 | 10 | import Foundation 11 | 12 | enum ExplainError: Error, LocalizedError { 13 | case unrecognizedID(String) 14 | 15 | var errorDescription: String? { 16 | switch self { 17 | case .unrecognizedID(let summary): 18 | return summary 19 | } 20 | } 21 | } 22 | 23 | public func explain(_ arguments: [String]) throws { 24 | var unrecognizedIDs = [String]() 25 | 26 | for id in arguments { 27 | if let explainer = Explainer.all[id.uppercased()] ?? guessID(id).flatMap({ Explainer.all[$0] }) { 28 | print(plainText(for: explainer)) 29 | } else { 30 | unrecognizedIDs.append(id) 31 | } 32 | } 33 | 34 | if !unrecognizedIDs.isEmpty { 35 | throw ExplainError.unrecognizedID( 36 | "Unrecgonized ID\(unrecognizedIDs.count > 1 ? "s" : ""): \(unrecognizedIDs.joined(separator: ", ")). " + 37 | "Please choose from: \(Explainer.all.keys.sorted().joined(separator: ", "))" 38 | ) 39 | } 40 | } 41 | 42 | private func guessID(_ badID: String) -> String? { 43 | let maybeNumber: String 44 | if badID.uppercased().first == "E" { 45 | maybeNumber = String(badID.dropFirst()) 46 | } else { 47 | maybeNumber = badID 48 | } 49 | 50 | guard let n = Int(maybeNumber) else { 51 | return nil 52 | } 53 | 54 | if n < 10 { 55 | return "E00\(n)" 56 | } else if n < 100 { 57 | return "E0\(n)" 58 | } else { 59 | return "E\(n)" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/DrStringCore/extract.swift: -------------------------------------------------------------------------------- 1 | import Crawler 2 | import Critic 3 | import Informant 4 | import IsTTY 5 | import Pathos 6 | import Models 7 | import Decipher 8 | #if canImport(Darwin) 9 | import Darwin 10 | #else 11 | import Glibc 12 | #endif 13 | import Dispatch 14 | import Foundation 15 | 16 | struct Documented: Codable { 17 | let documentable: Documentable 18 | let docstring: DocString 19 | } 20 | 21 | public func extract(with config: Configuration) throws { 22 | if config.includedPaths.isEmpty { 23 | throw CheckError.missingInput 24 | } 25 | 26 | let serialQueue = DispatchQueue(label: "ca.duan.DrString.serial") 27 | var hasOutput = false 28 | let group = DispatchGroup() 29 | let queue = DispatchQueue(label: "ca.duan.DrString.concurrent", attributes: .concurrent) 30 | 31 | let (included, _) = expandGlob(patterns: config.includedPaths.map(Path.init)) 32 | let (excluded, _) = expandGlob(patterns: config.excludedPaths.map(Path.init)) 33 | 34 | print("[", terminator: "") 35 | for path in included { 36 | let isPathExcluded = excluded.contains(path) 37 | guard !(isPathExcluded && config.allowSuperfluousExclusion) else { 38 | continue 39 | } 40 | 41 | queue.async(group: group) { 42 | do { 43 | let (documentables, _) = try extractDocs(fromSource: path) 44 | for documentable in documentables.compactMap({ $0 }) { 45 | if !documentable.docLines.isEmpty, 46 | let docstring = try? parse( 47 | location: .init(path: documentable.path, line: documentable.startLine - documentable.docLines.count), 48 | lines: documentable.docLines 49 | ) 50 | { 51 | let documented = Documented(documentable: documentable, docstring: docstring) 52 | if let json = try? JSONEncoder().encode(documented), 53 | let jsonString = String(data: json, encoding: .utf8) 54 | { 55 | serialQueue.async { 56 | if hasOutput { 57 | print(",") 58 | } else { 59 | hasOutput = true 60 | } 61 | 62 | print(jsonString) 63 | } 64 | } 65 | } 66 | } 67 | } catch {} 68 | } 69 | 70 | } 71 | 72 | group.wait() 73 | print("]", terminator: "") 74 | } 75 | -------------------------------------------------------------------------------- /Sources/DrStringCore/format.swift: -------------------------------------------------------------------------------- 1 | import Crawler 2 | import Dispatch 3 | import Editor 4 | import IsTTY 5 | import Pathos 6 | 7 | #if canImport(Darwin) 8 | import Darwin 9 | #else 10 | import Glibc 11 | #endif 12 | import Foundation 13 | 14 | enum FormatError: Error, LocalizedError { 15 | case missingInput 16 | 17 | var errorDescription: String? { 18 | switch self { 19 | case .missingInput: 20 | return "Paths to source files are missing. Please provide some." 21 | } 22 | } 23 | } 24 | 25 | public func formatEdits(fromSource source: String, path: Path? = nil, with config: Configuration) throws -> [Edit] { 26 | var edits = [Edit]() 27 | let documentables = try extractDocs(fromSource: source, sourcePath: path) 28 | for documentable in documentables.compactMap({ $0 }) { 29 | edits += documentable.format( 30 | columnLimit: config.columnLimit, 31 | verticalAlign: config.verticalAlignParameterDescription, 32 | alignAfterColon: config.alignAfterColon, 33 | firstLetterUpperCase: config.firstKeywordLetter == .uppercase, 34 | parameterStyle: config.parameterStyle, 35 | separations: config.separatedSections, 36 | ignoreThrows: config.ignoreDocstringForThrows, 37 | ignoreReturns: config.ignoreDocstringForReturns, 38 | addPlaceholder: config.addPlaceholder, 39 | startLine: config.startLine, 40 | endLine: config.endLine) 41 | } 42 | 43 | return edits 44 | } 45 | 46 | public func format(with config: Configuration) throws { 47 | if config.includedPaths.isEmpty { 48 | throw FormatError.missingInput 49 | } 50 | 51 | let startTime = getTime() 52 | var editCount = 0 53 | var fileCount = 0 54 | 55 | let included = Set((config.includedPaths.compactMap { try? Path($0).glob() }).flatMap { $0 }) 56 | let excluded = Set((config.excludedPaths.compactMap { try? Path($0).glob() }).flatMap { $0 }) 57 | 58 | let group = DispatchGroup() 59 | let queue = DispatchQueue.global() 60 | for path in included.subtracting(excluded) { 61 | group.enter() 62 | queue.async { 63 | do { 64 | let source = try path.readUTF8String() 65 | let edits = try formatEdits(fromSource: source, path: path, with: config) 66 | 67 | if !edits.isEmpty { 68 | var editedLines = [String]() 69 | let originalLines = source.split(separator: "\n", omittingEmptySubsequences: false) 70 | .map(String.init) 71 | var lastPosition = 0 72 | for edit in edits { 73 | editedLines += originalLines[lastPosition ..< edit.startingLine] 74 | lastPosition = edit.endingLine 75 | editedLines += edit.text 76 | } 77 | 78 | editedLines += originalLines[lastPosition...] 79 | 80 | let finalText = editedLines.joined(separator: "\n") 81 | try path.write(utf8: finalText) 82 | editCount += edits.count 83 | } 84 | } catch let error { 85 | fatalError("\(error): \(path)") 86 | } 87 | 88 | group.leave() 89 | } 90 | 91 | fileCount += 1 92 | } 93 | 94 | group.wait() 95 | let elapsedTime = readableDiff(from: startTime, to: getTime()) 96 | if editCount > 0 { 97 | let summary: String 98 | if IsTerminal.standardOutput { 99 | summary = "Fixed \(String(editCount), color: .red) docstring\(editCount > 1 ? "s" : "") in \(String(fileCount), color: .blue) file\(fileCount > 1 ? "s" : "") in \(elapsedTime, color: .blue)\n" 100 | } else { 101 | summary = "Fixed \(editCount) docstring\(editCount > 1 ? "s" : "") in \(fileCount) file\(fileCount > 1 ? "s" : "") in \(elapsedTime)\n" 102 | } 103 | 104 | fputs(summary, stderr) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/DrStringCore/version.swift: -------------------------------------------------------------------------------- 1 | public let version = "0.5.2" 2 | -------------------------------------------------------------------------------- /Sources/Editor/Documentable+Editing.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | import Decipher 3 | 4 | extension Documentable { 5 | public func format( 6 | columnLimit: Int?, 7 | verticalAlign: Bool, 8 | alignAfterColon: [Section], 9 | firstLetterUpperCase: Bool, 10 | parameterStyle: ParameterStyle, 11 | separations: [Section], 12 | ignoreThrows: Bool, 13 | ignoreReturns: Bool, 14 | addPlaceholder: Bool, 15 | startLine: Int?, 16 | endLine: Int? 17 | ) -> [Edit] 18 | { 19 | if let startLine = startLine, let endLine = endLine, 20 | self.endLine < startLine || (self.startLine - self.docLines.count) > endLine 21 | { 22 | return [] 23 | } 24 | 25 | let perhapsDocs = self.docLines.isEmpty && addPlaceholder ? nil : try? parse(location: .init(), 26 | lines: self.docLines) 27 | if !addPlaceholder && perhapsDocs == nil { 28 | return [] 29 | } 30 | 31 | var docs = perhapsDocs ?? DocString( 32 | location: .init(), 33 | description: [], parameterHeader: nil, parameters: [], returns: nil, throws: nil) 34 | 35 | if addPlaceholder { 36 | self.addDescription(to: &docs) 37 | self.addParameters(to: &docs) 38 | self.addThrows(to: &docs, ignoreThrows: ignoreThrows) 39 | self.addReturns(to: &docs, ignoreReturns: ignoreReturns) 40 | } 41 | 42 | let formatted = docs.reformat( 43 | initialColumn: self.startColumn, 44 | columnLimit: columnLimit, 45 | verticalAlign: verticalAlign, 46 | alignAfterColon: alignAfterColon, 47 | firstLetterUpperCase: firstLetterUpperCase, 48 | parameterStyle: parameterStyle, 49 | separations: separations) 50 | 51 | if formatted != self.docLines { 52 | let padding = String(Array(repeating: " ", count: self.startColumn)) 53 | return [ 54 | Edit( 55 | startingLine: self.startLine - self.docLines.count, 56 | endingLine: self.startLine, 57 | text: formatted.map { padding + $0 }) 58 | ] 59 | } else { 60 | return [] 61 | } 62 | } 63 | 64 | private func addDescription(to docs: inout DocString) { 65 | if docs.description.isEmpty { 66 | docs.description.append(.init(" ", "<#\(self.name)#>")) 67 | } 68 | } 69 | 70 | private func addParameters(to docs: inout DocString) { 71 | let parameters = self.details.parameters 72 | 73 | var parameterDocs = [DocString.Entry]() 74 | 75 | let commonality = commonSequence(parameters, docs) 76 | var commonIter = commonality.makeIterator() 77 | var nextCommon = commonIter.next() 78 | var docsIter = docs.parameters.makeIterator() 79 | var nextDoc = docsIter.next() 80 | 81 | for param in parameters { 82 | if param == nextCommon { 83 | while let doc = nextDoc, doc.name.text != param.name { 84 | parameterDocs.append(doc) 85 | nextDoc = docsIter.next() 86 | } 87 | 88 | if let doc = nextDoc { 89 | parameterDocs.append(doc) 90 | nextDoc = docsIter.next() 91 | } 92 | 93 | nextCommon = commonIter.next() 94 | } else { 95 | parameterDocs.append( 96 | .init( 97 | relativeLineNumber: 0, 98 | preDashWhitespaces: "", 99 | keyword: nil, 100 | name: .init("", param.name), 101 | preColonWhitespace: "", 102 | hasColon: true, 103 | description: [.init("", "<#\(param.type)#>")])) 104 | } 105 | } 106 | 107 | docs.parameters = parameterDocs 108 | } 109 | 110 | private func addThrows(to docs: inout DocString, ignoreThrows: Bool) { 111 | let doesThrow = self.details.throws 112 | if doesThrow && docs.throws == nil && !ignoreThrows { 113 | docs.throws = .init( 114 | relativeLineNumber: 0, 115 | preDashWhitespaces: "", 116 | keyword: nil, 117 | name: .empty, 118 | preColonWhitespace: "", 119 | hasColon: true, 120 | description: [.init(" ", "<#Error#>")] 121 | ) 122 | } 123 | } 124 | 125 | private func addReturns(to docs: inout DocString, ignoreReturns: Bool) { 126 | let returnType = self.details.returnType 127 | if let returnType = returnType, docs.returns == nil && !ignoreReturns { 128 | docs.returns = .init( 129 | relativeLineNumber: 0, 130 | preDashWhitespaces: "", 131 | keyword: nil, 132 | name: .empty, 133 | preColonWhitespace: "", 134 | hasColon: true, 135 | description: [.init(" ", "<#\(returnType)#>")] 136 | ) 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Sources/Editor/Edit.swift: -------------------------------------------------------------------------------- 1 | /// A change to a file. All changes are assumed to be a range of lines. 2 | public struct Edit { 3 | public let startingLine: Int 4 | public let endingLine: Int 5 | public let text: [String] 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Informant/PlainTextFormatter.swift: -------------------------------------------------------------------------------- 1 | import Critic 2 | import Pathos 3 | 4 | private extension DocProblem.Detail { 5 | private func actualWhitespace(_ actual: String) -> String { 6 | actual.isEmpty ? "none" : "`\(actual)`" 7 | } 8 | 9 | private var description: String { 10 | switch self { 11 | case .redundantParameter(let name): 12 | return "Unrecognized docstring for `\(name)`" 13 | case .missingParameter(let name, let type): 14 | return "Missing docstring for `\(name)` of type `\(type)`" 15 | case .missingThrow: 16 | return "Missing docstring for throws" 17 | case .missingReturn(let type): 18 | return "Missing docstring for return type `\(type)`" 19 | case .preDashSpaceInParameter(let expected, let actual, let name): 20 | return "Parameter `\(name)` should start with exactly \(expected) space\(expected > 1 ? "s" : "") before `-`, found `\(actual)`" 21 | case .spaceBetweenDashAndParameterKeyword(let actual, let keyword, let name): 22 | return "`\(name)` should have exactly 1 space between `-` and `\(keyword)`, found \(actualWhitespace(actual))" 23 | case .spaceBeforeParameterName(let actual, let keyword, let name): 24 | return "There should be exactly 1 space between `\(keyword)` and `\(name)`, found \(actualWhitespace(actual))" 25 | case .spaceBeforeColon(let actual, let name): 26 | return "For `\(name)`, there should be no whitespace before `:`, found `\(actual)`" 27 | case .preDashSpace(let keyword, let actual): 28 | return "`\(keyword)` should start with exactly 1 space before `-`, found \(actualWhitespace(actual))" 29 | case .spaceBetweenDashAndKeyword(let keyword, let actual): 30 | return "There should be exactly 1 space between `-` and `\(keyword)`, found \(actualWhitespace(actual))" 31 | case .verticalAlignment(let expected, let nameOrKeyword, let line): 32 | return "Line \(line) of `\(nameOrKeyword)`'s description is not properly vertically aligned (should have \(expected) leading spaces)" 33 | case .spaceAfterColon(let keyword, let actual): 34 | return "For `\(keyword)`, there should be exactly 1 space after `:`, found \(actualWhitespace(actual))" 35 | case .keywordCasingForParameter(let actual, let expected, let name): 36 | return "For `\(name)`, `\(expected)` is misspelled as `\(actual)`" 37 | case .keywordSpelling(let actual, let expected): 38 | return "`\(expected)` is misspelled as `\(actual)`" 39 | case .descriptionShouldEndWithEmptyLine: 40 | return "Overall description should end with an empty line" 41 | case .sectionShouldEndWithEmptyLine(let keywordOrName): 42 | return "`\(keywordOrName)`'s description should end with an empty line" 43 | case .redundantKeyword(let keyword): 44 | return "Redundant documentation for `\(keyword)`" 45 | case .redundantTextFollowingParameterHeader(let keyword): 46 | return "`:` should be the last character on the line for `\(keyword)`" 47 | case .excludedYetNoProblemIsFound(let configFile): 48 | let exclusionHint = " in \(configFile ?? "a command line argument")" 49 | return "This file is explicitly excluded\(exclusionHint), but it has no docstring problems (except for this)." 50 | case .excludedYetNotIncluded: 51 | return "This file is explicitly excluded, but it's not included for checking anyways." 52 | case .parametersAreNotGrouped: 53 | return "Parameters are organized in the \"separate\" style, but \"grouped\" style is preferred." 54 | case .parametersAreNotSeparated: 55 | return "Parameters are organized in the \"grouped\" style, but \"separate\" style is preferred." 56 | case .missingColon(let name): 57 | return "`\(name)` should be followed by a `:` character." 58 | case .invalidPattern(let type, let configFile): 59 | let source = "\(configFile ?? "a command line argument")" 60 | return "Could not find any files matching this \(type) pattern specified in \(source)." 61 | } 62 | } 63 | 64 | var fullDescription: String { "|\(self.explainerID)| \(self.description) " } 65 | } 66 | 67 | private extension DocProblem { 68 | var description: String { 69 | let path = (try? Path(self.filePath).absolute()).map { "\($0)" } ?? self.filePath 70 | return "\(path):\(self.line + 1):\(self.column): warning: \(self.detail.fullDescription)" 71 | } 72 | } 73 | 74 | public func plainText(for docProblem: DocProblem) -> String { 75 | return docProblem.description 76 | } 77 | 78 | let kExplainerSeparator = "\n------------------------------------\n" 79 | let kExplainerBorder = "\n====================================\n" 80 | 81 | 82 | private extension Explainer { 83 | var description: String { 84 | [ 85 | "\n========== DrString \(self.id) ===========\n", 86 | "Summary:\(kExplainerSeparator)\(self.summary)\(kExplainerSeparator)", 87 | self.wrongExample.map { "Bad example:\(kExplainerSeparator)\($0)\(kExplainerSeparator)" }, 88 | self.rightExample.map { "Good example:\(kExplainerSeparator)\($0)\(kExplainerSeparator)" }, 89 | ] 90 | .compactMap { $0 } 91 | .joined(separator: "\n") 92 | + kExplainerBorder 93 | } 94 | } 95 | 96 | public func plainText(for explainer: Explainer) -> String { 97 | return explainer.description 98 | } 99 | -------------------------------------------------------------------------------- /Sources/Informant/TtyTextFormatter.swift: -------------------------------------------------------------------------------- 1 | import Chalk 2 | import Critic 3 | import Pathos 4 | 5 | private extension DocProblem.Detail { 6 | private func actualWhitespace(_ actual: String) -> String { 7 | actual.isEmpty ? "none" : "\(actual, background: .cyan)" 8 | } 9 | 10 | private var description: String { 11 | switch self { 12 | case .redundantParameter(let name): 13 | return "Unrecognized docstring for \(name, color: .green)" 14 | case .missingParameter(let name, let type): 15 | return "Missing docstring for \(name, color: .green) of type \(type, color: .cyan)" 16 | case .missingThrow: 17 | return "Missing docstring for throws" 18 | case .missingReturn(let type): 19 | return "Missing docstring for return type \(type, color: .cyan)" 20 | case .preDashSpaceInParameter(let expected, let actual, let name): 21 | return "Parameter \(name, color: .green) should start with exactly \(expected, color: .cyan) space\(expected > 1 ? "s" : "") before \("-", color: .green), found \(actualWhitespace(actual))" 22 | case .spaceBetweenDashAndParameterKeyword(let actual, let keyword, let name): 23 | return "\(name, color: .green) should have exactly 1 space between \("-", color: .green) and \(keyword, color: .green), found \(actualWhitespace(actual))" 24 | case .spaceBeforeParameterName(let actual, let keyword, let name): 25 | return "There should be exactly 1 space between \(keyword, color: .green) and \(name, color: .green), found \(actualWhitespace(actual))" 26 | case .spaceBeforeColon(let actual, let name): 27 | return "For \(name, color: .green), there should be no whitespace before \(":", color: .green), found \(actual, background: .cyan)" 28 | case .preDashSpace(let keyword, let actual): 29 | return "\(keyword, color: .green) should start with exactly 1 space before \("-", color: .green), found \(actualWhitespace(actual))" 30 | case .spaceBetweenDashAndKeyword(let keyword, let actual): 31 | return "There should be exactly 1 space between \("-", color: .green) and \(keyword, color: .green), found \(actualWhitespace(actual))" 32 | case .verticalAlignment(let expected, let nameOrKeyword, let line): 33 | return "Line \(line, color: .green) of \(nameOrKeyword, color: .green)'s description is not properly vertically aligned (should have \(expected, color: .green) leading spaces)" 34 | case .spaceAfterColon(let keyword, let actual): 35 | return "For \(keyword, color: .green), there should be exactly 1 space after \(":", color: .green), found \(actualWhitespace(actual))" 36 | case .keywordCasingForParameter(let actual, let expected, let name): 37 | return "For \(name, color: .green), \(expected, color: .green) is misspelled as \(actual, color: .cyan)" 38 | case .keywordSpelling(let actual, let expected): 39 | return "\(expected, color: .green) is misspelled as \(actual, color: .cyan)" 40 | case .descriptionShouldEndWithEmptyLine: 41 | return "Overall description should end with an empty line" 42 | case .sectionShouldEndWithEmptyLine(let keywordOrName): 43 | return "\(keywordOrName, color: .green)'s description should end with an empty line" 44 | case .redundantKeyword(let keyword): 45 | return "Redundant documentation for \(keyword, color: .green)" 46 | case .redundantTextFollowingParameterHeader(let keyword): 47 | return "\(":", color: .green) should be the last character on the line for \(keyword, color: .green)" 48 | case .excludedYetNoProblemIsFound(let configFile): 49 | let exclusionHint = " in \(configFile ?? "a command line argument")" 50 | return "This file is explicitly excluded\(exclusionHint), but it has no docstring problems (except for this)." 51 | case .excludedYetNotIncluded: 52 | return "This file is explicitly excluded, but it's not included for checking anyways." 53 | case .parametersAreNotGrouped: 54 | return "Parameters are organized in the \("separate", color: .green) style, but \("grouped", color: .green) style is preferred." 55 | case .parametersAreNotSeparated: 56 | return "Parameters are organized in the \("grouped", color: .green) style, but \("separate", color: .green) style is preferred." 57 | case .missingColon(let name): 58 | return "\(name, color: .green) should be followed by a \(":", color: .green) character." 59 | case .invalidPattern(let type, let configFile): 60 | let source = "\(configFile ?? "a command line argument")" 61 | return "Could not find any files matching this \(type) pattern specified in \(source)." 62 | } 63 | } 64 | 65 | private var id: String { "\(self.explainerID, color: .blue)" } 66 | 67 | var fullDescription: String { "|\(self.id)| \(self.description) " } 68 | } 69 | 70 | private extension DocProblem { 71 | var description: String { 72 | let path = (try? Path(self.filePath).absolute()).map { "\($0)" } ?? self.filePath 73 | let warning = "\("warning", color: .yellow)" 74 | let header = "\(path):\(self.line + 1):\(self.column):" 75 | return "\(header, style: .bold) \(warning): \(self.detail.fullDescription)" 76 | } 77 | } 78 | 79 | public func ttyText(for docProblem: DocProblem) -> String { 80 | return docProblem.description 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Models/AbsoluteSourceLocation.swift: -------------------------------------------------------------------------------- 1 | public struct AbsoluteSourceLocation: Equatable, Codable { 2 | public var path: String 3 | public var line: Int 4 | public var column: Int 5 | 6 | public init(path: String = "", line: Int = 0, column: Int = 0) { 7 | self.path = path 8 | self.line = line 9 | self.column = column 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Models/DocString.swift: -------------------------------------------------------------------------------- 1 | public struct DocString: Equatable, Codable { 2 | public struct Entry: Equatable, Codable { 3 | public let relativeLineNumber: Int 4 | public let preDashWhitespace: String 5 | public let keyword: TextLeadByWhitespace? 6 | public let name: TextLeadByWhitespace 7 | public let preColonWhitespace: String 8 | public let hasColon: Bool 9 | public let description: [TextLeadByWhitespace] 10 | 11 | public init( 12 | relativeLineNumber: Int = 0, 13 | preDashWhitespaces: String, 14 | keyword: TextLeadByWhitespace?, 15 | name: TextLeadByWhitespace, 16 | preColonWhitespace: String, 17 | hasColon: Bool, 18 | description: [TextLeadByWhitespace] 19 | ) { 20 | self.relativeLineNumber = relativeLineNumber 21 | self.preDashWhitespace = preDashWhitespaces 22 | self.keyword = keyword 23 | self.name = name 24 | self.preColonWhitespace = preColonWhitespace 25 | self.hasColon = hasColon 26 | self.description = description 27 | } 28 | } 29 | 30 | public var location: AbsoluteSourceLocation 31 | public var description: [TextLeadByWhitespace] 32 | public var parameterHeader: Entry? 33 | public var parameters: [Entry] 34 | public var returns: Entry? 35 | public var `throws`: Entry? 36 | 37 | public init( 38 | location: AbsoluteSourceLocation, 39 | description: [TextLeadByWhitespace], 40 | parameterHeader: Entry?, 41 | parameters: [Entry], 42 | returns: Entry?, 43 | throws: Entry? 44 | ) { 45 | self.location = location 46 | self.description = description 47 | self.parameterHeader = parameterHeader 48 | self.parameters = parameters 49 | self.returns = returns 50 | self.throws = `throws` 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Models/Documentable.swift: -------------------------------------------------------------------------------- 1 | public struct Parameter: Equatable, Codable { 2 | public let label: String? 3 | public let name: String 4 | public let type: String 5 | public let isVariadic: Bool 6 | public let hasDefault: Bool 7 | 8 | public init( 9 | label: String?, 10 | name: String, 11 | type: String, 12 | isVariadic: Bool, 13 | hasDefault: Bool) 14 | { 15 | self.label = label 16 | self.name = name 17 | self.type = type 18 | self.isVariadic = isVariadic 19 | self.hasDefault = hasDefault 20 | } 21 | } 22 | 23 | public struct Documentable: Equatable, Codable { 24 | public let path: String 25 | public let startLine: Int 26 | public let startColumn: Int 27 | public let endLine: Int 28 | public let name: String 29 | public let docLines: [String] 30 | public let children: [Documentable] 31 | public let details: Details 32 | 33 | public struct Details: Equatable, Codable { 34 | public let `throws`: Bool 35 | public let returnType: String? 36 | public let parameters: [Parameter] 37 | 38 | public init(throws: Bool, returnType: String?, parameters: [Parameter]) { 39 | self.throws = `throws` 40 | self.returnType = returnType 41 | self.parameters = parameters 42 | } 43 | } 44 | 45 | public init( 46 | path: String, 47 | startLine: Int, 48 | startColumn: Int, 49 | endLine: Int, 50 | name: String, 51 | docLines: [String], 52 | children: [Documentable], 53 | details: Details) 54 | { 55 | self.path = path 56 | self.startLine = startLine 57 | self.startColumn = startColumn 58 | self.endLine = endLine 59 | self.name = name 60 | self.docLines = docLines 61 | self.children = children 62 | self.details = details 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Models/ParameterStyle.swift: -------------------------------------------------------------------------------- 1 | public enum ParameterStyle: String, Equatable, Codable { 2 | case grouped 3 | case separate 4 | case whatever 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Models/Section.swift: -------------------------------------------------------------------------------- 1 | public enum Section: String, Hashable, Codable, CaseIterable { 2 | case description 3 | case parameters 4 | case `throws` 5 | case returns 6 | } 7 | 8 | -------------------------------------------------------------------------------- /Sources/Models/StringLeadByWhitespace.swift: -------------------------------------------------------------------------------- 1 | public struct TextLeadByWhitespace: Equatable, Codable { 2 | public let lead: String 3 | public let text: String 4 | 5 | public init(_ lead: String, _ text: String) { 6 | self.lead = lead 7 | self.text = text 8 | } 9 | 10 | public static let empty = TextLeadByWhitespace("", "") 11 | } 12 | 13 | extension TextLeadByWhitespace: CustomStringConvertible { 14 | public var description: String { 15 | "\(self.lead)\(self.text)" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Models/algorithms.swift: -------------------------------------------------------------------------------- 1 | public func commonSequence(_ parameters: [Parameter], _ docs: DocString) -> [Parameter] { 2 | var cache = [Int: [Int: [Parameter]]]() 3 | func lcs(_ sig: [Parameter], _ sigIndex: Int, _ doc: [DocString.Entry], 4 | _ docIndex: Int) -> [Parameter] 5 | { 6 | if let cached = cache[sigIndex]?[docIndex] { 7 | return cached 8 | } 9 | 10 | guard sigIndex < sig.count && docIndex < doc.count else { 11 | return [] 12 | } 13 | 14 | if sig[sigIndex].name == doc[docIndex].name.text { 15 | return [sig[sigIndex]] + lcs(sig, sigIndex + 1, doc, docIndex) 16 | } 17 | 18 | let a = lcs(sig, sigIndex + 1, doc, docIndex) 19 | let b = lcs(sig, sigIndex, doc, docIndex + 1) 20 | 21 | let result = a.count > b.count ? a : b 22 | cache[sigIndex] = cache[sigIndex, default: [:]].merging([docIndex: result]) { $1 } 23 | 24 | return result 25 | } 26 | 27 | return lcs(parameters, 0, docs.parameters, 0) 28 | } 29 | -------------------------------------------------------------------------------- /Sources/_DrStringCore: -------------------------------------------------------------------------------- 1 | DrStringCore -------------------------------------------------------------------------------- /Tests/CLITests/CLITests.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | @testable import _DrStringCore 3 | @testable import DrStringCLI 4 | import Pathos 5 | import XCTest 6 | 7 | final class CLITests: XCTestCase { 8 | private let directory = Path(#file).parent 9 | 10 | func testConfigFileOptionsAreProperlyParsedForCheckSubcommand() throws { 11 | let configFilePath = self.directory + "Fixtures" + "config0.toml" 12 | let arguments = ["check", "--config-file", "\(configFilePath)"] 13 | let parsedCommand = try Main.parseAsRoot(arguments) 14 | let command = try Command(command: parsedCommand) 15 | XCTAssertEqual(command?.config?.firstKeywordLetter, .lowercase) 16 | XCTAssertEqual(command?.config?.separatedSections, [.description]) 17 | } 18 | 19 | func testCommandLineArgumentOverridesConfigFileOptionForCheckSubcommand() throws { 20 | let configFilePath = self.directory + "Fixtures" + "config0.toml" 21 | let arguments = ["check", "--config-file", "\(configFilePath)", "--first-letter", "uppercase"] 22 | let parsedCommand = try Main.parseAsRoot(arguments) 23 | let command = try Command(command: parsedCommand) 24 | XCTAssertEqual(command?.config?.firstKeywordLetter, .uppercase) 25 | XCTAssertEqual(command?.config?.separatedSections, [.description]) 26 | } 27 | 28 | func testConfigFileOptionsAreProperlyParsedForFormatSubcommand() throws { 29 | let configFilePath = self.directory + "Fixtures" + "config0.toml" 30 | let arguments = ["format", "--config-file", "\(configFilePath)"] 31 | let parsedCommand = try Main.parseAsRoot(arguments) 32 | let command = try Command(command: parsedCommand) 33 | XCTAssertEqual(command?.config?.firstKeywordLetter, .lowercase) 34 | XCTAssertEqual(command?.config?.separatedSections, [.description]) 35 | } 36 | 37 | func testCommandLineArgumentOverridesConfigFileOptionForFormatSubcommand() throws { 38 | let configFilePath = self.directory + "Fixtures" + "config0.toml" 39 | let arguments = ["format", "--config-file", "\(configFilePath)", "--first-letter", "uppercase"] 40 | let parsedCommand = try Main.parseAsRoot(arguments) 41 | let command = try Command(command: parsedCommand) 42 | XCTAssertEqual(command?.config?.firstKeywordLetter, .uppercase) 43 | XCTAssertEqual(command?.config?.separatedSections, [.description]) 44 | } 45 | 46 | func testCommandLineArgumentOverridesConfigFileOptionByExplicitNegation() throws { 47 | let configFilePath = self.directory + "Fixtures" + "config0.toml" 48 | let arguments = ["format", "--config-file", "\(configFilePath)", "--no-needs-separation"] 49 | let parsedCommand = try Main.parseAsRoot(arguments) 50 | let command = try Command(command: parsedCommand) 51 | XCTAssertEqual(command?.config?.separatedSections, .some([])) 52 | } 53 | 54 | func testStartLineIsProperlyParsed() throws { 55 | let line = 42 56 | let arguments = ["format", "--start-line", "\(line)", "-i", "a"] 57 | let parsedCommand = try Main.parseAsRoot(arguments) 58 | let command = try Command(command: parsedCommand) 59 | XCTAssertEqual(command?.config?.startLine, .some(line)) 60 | } 61 | 62 | func testEndLineIsProperlyParsed() throws { 63 | let line = 42 64 | let arguments = ["format", "--end-line", "\(line)", "-i", "a"] 65 | let parsedCommand = try Main.parseAsRoot(arguments) 66 | let command = try Command(command: parsedCommand) 67 | XCTAssertEqual(command?.config?.endLine, .some(line)) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/CLITests/Fixtures/config0.toml: -------------------------------------------------------------------------------- 1 | include = ["a"] 2 | first-letter = "lowercase" 3 | needs-separation = ["description"] 4 | -------------------------------------------------------------------------------- /Tests/CriticTests/AlignmentTests.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | @testable import Critic 3 | import XCTest 4 | 5 | final class AlignmentTests: XCTestCase { 6 | func testContinuationLineWithOneSpaceIndentMissingForVerticalAlign() throws { 7 | let parameterName = "name" 8 | let doc = DocString( 9 | location: .init(), 10 | description: [], 11 | parameterHeader: nil, 12 | parameters: [ 13 | .init( 14 | preDashWhitespaces: " ", 15 | keyword: .init(" ", "Parameter"), 16 | name: .init(" ", parameterName), 17 | preColonWhitespace: "", 18 | hasColon: true, 19 | description: [ 20 | .init(" ", "this is a parameter"), 21 | // - parameter name: 22 | .init(" ", "second line"), 23 | ] 24 | ) 25 | ], 26 | returns: nil, 27 | throws: nil) 28 | let problems = try findParameterProblems( 29 | fallback: .init(), 30 | 0, 31 | [ 32 | Parameter( 33 | label: nil, 34 | name: parameterName, 35 | type: "String", 36 | isVariadic: false, 37 | hasDefault: false 38 | )], 39 | doc, 40 | true, 41 | needsSeparation: false, 42 | verticalAlign: true, 43 | style: .separate, 44 | alignAfterColon: false) 45 | 46 | guard case .some(.verticalAlignment) = problems.first?.1 else { 47 | XCTFail("expected a missing space to be a problem") 48 | return 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/CriticTests/ParametersTests.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | @testable import Critic 3 | import XCTest 4 | 5 | final class ParametersTests: XCTestCase { 6 | func testMissingColonInParameterEntryIsReported() throws { 7 | let parameterName = "name" 8 | let doc = DocString( 9 | location: .init(), 10 | description: [], 11 | parameterHeader: nil, 12 | parameters: [ 13 | .init( 14 | preDashWhitespaces: " ", 15 | keyword: .init(" ", "Parameter"), 16 | name: .init(" ", parameterName), 17 | preColonWhitespace: "", 18 | hasColon: false, 19 | description: [ 20 | .init(" ", "this is a parameter"), 21 | ] 22 | ) 23 | ], 24 | returns: nil, 25 | throws: nil) 26 | let problems = try findParameterProblems( 27 | fallback: .init(), 28 | 0, 29 | [ 30 | Parameter( 31 | label: nil, 32 | name: parameterName, 33 | type: "String", 34 | isVariadic: false, 35 | hasDefault: false 36 | )], 37 | doc, 38 | true, 39 | needsSeparation: false, 40 | verticalAlign: false, 41 | style: .separate, 42 | alignAfterColon: false) 43 | 44 | guard case .some(.missingColon(let name)) = problems.first?.1, name == parameterName else { 45 | XCTFail("Expected problem is not reported") 46 | return 47 | } 48 | } 49 | 50 | func testMissingColonInHeaderIsReported() throws { 51 | let doc = DocString( 52 | location: .init(), 53 | description: [], 54 | parameterHeader: .init( 55 | preDashWhitespaces: " ", 56 | keyword: .init(" ", "Parameters"), 57 | name: .init("", ""), 58 | preColonWhitespace: "", 59 | hasColon: false, 60 | description: [] 61 | ), 62 | parameters: [], 63 | returns: nil, 64 | throws: nil) 65 | let problems = try findParameterProblems( 66 | fallback: .init(), 67 | 0, 68 | [], 69 | doc, 70 | true, 71 | needsSeparation: false, 72 | verticalAlign: false, 73 | style: .grouped, 74 | alignAfterColon: false) 75 | 76 | guard case .some(.missingColon(let keyword)) = problems.first?.1, keyword == "Parameters" else { 77 | XCTFail("Expected problem is not reported") 78 | return 79 | } 80 | } 81 | 82 | func testBadSpacingBetweenDashAndParameterName() throws { 83 | let doc = DocString( 84 | location: .init(), 85 | description: [], 86 | parameterHeader: .init( 87 | preDashWhitespaces: " ", 88 | keyword: .init(" ", "Parameters"), 89 | name: .init("", ""), 90 | preColonWhitespace: "", 91 | hasColon: true, 92 | description: [] 93 | ), 94 | parameters: [ 95 | .init( 96 | preDashWhitespaces: " ", 97 | keyword: nil, 98 | name: .init("", "a"), 99 | preColonWhitespace: "", 100 | hasColon: true, 101 | description: [] 102 | ), 103 | ], 104 | returns: nil, 105 | throws: nil) 106 | let problems = try findParameterProblems( 107 | fallback: .init(), 108 | 0, 109 | [ 110 | Parameter( 111 | label: nil, 112 | name: "a", 113 | type: "String", 114 | isVariadic: false, 115 | hasDefault: false 116 | ), 117 | ], 118 | doc, 119 | true, 120 | needsSeparation: false, 121 | verticalAlign: false, 122 | style: .grouped, 123 | alignAfterColon: false) 124 | 125 | guard case .some(.spaceBeforeParameterName(let actual, let keyword, let name)) = problems.first?.1 else { 126 | XCTFail("Expected problem is not reported") 127 | return 128 | } 129 | 130 | XCTAssertEqual(actual, "") 131 | XCTAssertEqual(name, "a") 132 | XCTAssertEqual(keyword, "-") 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Tests/CriticTests/ReturnsTests.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | @testable import Critic 3 | import XCTest 4 | 5 | final class ReturnsTests: XCTestCase { 6 | func testAlignAfterColonIsNotAProblem() { 7 | let returnDoc = DocString( 8 | location: .init(), 9 | description: [], 10 | parameterHeader: nil, 11 | parameters: [], 12 | returns: .init( 13 | preDashWhitespaces: " ", 14 | keyword: .init(" ", "Returns"), 15 | name: .init("", ""), 16 | preColonWhitespace: "", 17 | hasColon: true, 18 | description: [ 19 | .init(" ", "start"), 20 | .init(" ", "next line") 21 | ]), 22 | throws: nil) 23 | let problems = findReturnsProblems(fallback: .init(0, 0), ignoreReturns: false, returnDoc, 24 | returnType: "Int", firstLetterUpper: true, 25 | alignAfterColon: true) 26 | XCTAssert(problems.isEmpty) 27 | } 28 | 29 | func testAlignBeforeColonIsAProblem() { 30 | let returnDoc = DocString( 31 | location: .init(), 32 | description: [], 33 | parameterHeader: nil, 34 | parameters: [], 35 | returns: .init( 36 | preDashWhitespaces: " ", 37 | keyword: .init(" ", "Returns"), 38 | name: .init("", ""), 39 | preColonWhitespace: "", 40 | hasColon: true, 41 | description: [ 42 | .init(" ", "start"), 43 | .init(" ", "next line") 44 | ]), 45 | throws: nil) 46 | let problems = findReturnsProblems(fallback: .init(0, 0), ignoreReturns: false, returnDoc, 47 | returnType: "Int", firstLetterUpper: true, alignAfterColon: true) 48 | 49 | guard case .some(.verticalAlignment(12, "Returns", 2)) = problems.first?.1 else { 50 | XCTFail("expected vertical aligment problem") 51 | return 52 | } 53 | } 54 | 55 | func testMissingColonIsAProblem() { 56 | let returnDoc = DocString( 57 | location: .init(), 58 | description: [], 59 | parameterHeader: nil, 60 | parameters: [], 61 | returns: .init( 62 | preDashWhitespaces: " ", 63 | keyword: .init(" ", "Returns"), 64 | name: .init("", ""), 65 | preColonWhitespace: "", 66 | hasColon: false, 67 | description: [ 68 | .init(" ", "start"), 69 | ]), 70 | throws: nil) 71 | let problems = findReturnsProblems(fallback: .init(0, 0), ignoreReturns: false, returnDoc, 72 | returnType: "Int", firstLetterUpper: true, alignAfterColon: true) 73 | guard case .some(.missingColon("Returns")) = problems.first?.1 else { 74 | XCTFail("expected missing colon to be a problem") 75 | return 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/CriticTests/ThrowsTests.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | @testable import Critic 3 | import XCTest 4 | 5 | final class ThrowsTests: XCTestCase { 6 | func testAlignAfterColonIsNotAProblem() { 7 | let throwsDoc = DocString( 8 | location: .init(), 9 | description: [], 10 | parameterHeader: nil, 11 | parameters: [], 12 | returns: nil, 13 | throws: .init( 14 | preDashWhitespaces: " ", 15 | keyword: .init(" ", "Throws"), 16 | name: .init("", ""), 17 | preColonWhitespace: "", 18 | hasColon: true, 19 | description: [ 20 | .init(" ", "start"), 21 | .init(" ", "next line") 22 | ])) 23 | let problems = findThrowsProblems(fallback: .init(0, 0), ignoreThrows: false, doesThrow: true, 24 | throwsDoc, firstLetterUpper: true, needsSeparation: false, 25 | alignAfterColon: true) 26 | XCTAssert(problems.isEmpty) 27 | } 28 | 29 | func testAlignBeforeColonIsAProblem() { 30 | let throwsDoc = DocString( 31 | location: .init(), 32 | description: [], 33 | parameterHeader: nil, 34 | parameters: [], 35 | returns: nil, 36 | throws: .init( 37 | preDashWhitespaces: " ", 38 | keyword: .init(" ", "Throws"), 39 | name: .init("", ""), 40 | preColonWhitespace: "", 41 | hasColon: true, 42 | description: [ 43 | .init(" ", "start"), 44 | .init(" ", "next line") 45 | ])) 46 | let problems = findThrowsProblems(fallback: .init(0, 0), ignoreThrows: false, doesThrow: true, 47 | throwsDoc, firstLetterUpper: true, needsSeparation: false, 48 | alignAfterColon: true) 49 | 50 | guard case .some(.verticalAlignment(11, "Throws", 2)) = problems.first?.1 else { 51 | XCTFail("expected vertical aligment problem") 52 | return 53 | } 54 | } 55 | 56 | func testMissingColonIsAProblem() { 57 | let throwsDoc = DocString( 58 | location: .init(), 59 | description: [], 60 | parameterHeader: nil, 61 | parameters: [], 62 | returns: nil, 63 | throws: .init( 64 | preDashWhitespaces: " ", 65 | keyword: .init(" ", "Throws"), 66 | name: .init("", ""), 67 | preColonWhitespace: "", 68 | hasColon: false, 69 | description: [ 70 | .init(" ", "start"), 71 | ])) 72 | let problems = findThrowsProblems(fallback: .init(0, 0), ignoreThrows: false, doesThrow: true, 73 | throwsDoc, firstLetterUpper: true, needsSeparation: false, 74 | alignAfterColon: true) 75 | guard case .some(.missingColon("Throws")) = problems.first?.1 else { 76 | XCTFail("expected missing colon to be a problem") 77 | return 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/DecipherTests/StatefulParsingTests.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | @testable import Decipher 3 | import XCTest 4 | 5 | final class StatefulParsingTests: XCTestCase { 6 | func testNormalGroupedParameterState() throws { 7 | let text = """ 8 | /// Overall description 9 | /// 10 | /// - Parameters: 11 | /// - d: d description 12 | /// - c: c description 13 | /// - b: b description 14 | /// - a: a description 15 | /// a description continues 16 | /// - Returns: Returns description 17 | /// - Throws: Throws description 18 | """ 19 | 20 | let expected = DocString( 21 | location: .init(path: "", line: 0, column: 0), 22 | description: [ 23 | .init(" ", "Overall description"), 24 | .empty, 25 | ], 26 | parameterHeader: .init( 27 | relativeLineNumber: 2, 28 | preDashWhitespaces: " ", 29 | keyword: .init(" ", "Parameters"), 30 | name: .init("", ""), 31 | preColonWhitespace: "", 32 | hasColon: true, 33 | description: [] 34 | ), 35 | parameters: [ 36 | .init( 37 | relativeLineNumber: 3, 38 | preDashWhitespaces: " ", 39 | keyword: nil, 40 | name: .init(" ", "d"), 41 | preColonWhitespace: "", 42 | hasColon: true, 43 | description: [ 44 | .init(" ", "d description") 45 | ] 46 | ), 47 | .init( 48 | relativeLineNumber: 4, 49 | preDashWhitespaces: " ", 50 | keyword: nil, 51 | name: .init(" ", "c"), 52 | preColonWhitespace: "", 53 | hasColon: true, 54 | description: [ 55 | .init(" ", "c description") 56 | ] 57 | ), 58 | .init( 59 | relativeLineNumber: 5, 60 | preDashWhitespaces: " ", 61 | keyword: nil, 62 | name: .init(" ", "b"), 63 | preColonWhitespace: "", 64 | hasColon: true, 65 | description: [ 66 | .init(" ", "b description") 67 | ] 68 | ), 69 | .init( 70 | relativeLineNumber: 6, 71 | preDashWhitespaces: " ", 72 | keyword: nil, 73 | name: .init(" ", "a"), 74 | preColonWhitespace: "", 75 | hasColon: true, 76 | description: [ 77 | .init(" ", "a description"), 78 | .init(" ", "a description continues"), 79 | ] 80 | ), 81 | ], 82 | returns: .init( 83 | relativeLineNumber: 8, 84 | preDashWhitespaces: " ", 85 | keyword: .init(" ", "Returns"), 86 | name: .empty, 87 | preColonWhitespace: "", 88 | hasColon: true, 89 | description: [.init(" ", "Returns description")] 90 | ), 91 | throws: .init( 92 | relativeLineNumber: 9, 93 | preDashWhitespaces: " ", 94 | keyword: .init(" ", "Throws"), 95 | name: .empty, 96 | preColonWhitespace: "", 97 | hasColon: true, 98 | description: [.init(" ", "Throws description")] 99 | ) 100 | ) 101 | 102 | let actual = try parse(location: .init(), lines: text.split(separator: "\n").map(String.init)) 103 | 104 | XCTAssertEqual(actual, expected) 105 | } 106 | 107 | func testNormalSeparateParameterState() throws { 108 | let text = """ 109 | /// Overall description 110 | /// 111 | /// - Parameter d: d description 112 | /// - Parameter c: c description 113 | /// - Parameter b: b description 114 | /// - Parameter a: a description 115 | /// a description continues 116 | /// - Returns: Returns description 117 | /// - Throws: Throws description 118 | """ 119 | 120 | let expected = DocString( 121 | location: .init(), 122 | description: [ 123 | .init(" ", "Overall description"), 124 | .empty 125 | ], 126 | parameterHeader: nil, 127 | parameters: [ 128 | .init( 129 | relativeLineNumber: 2, 130 | preDashWhitespaces: " ", 131 | keyword: .init(" ", "Parameter"), 132 | name: .init(" ", "d"), 133 | preColonWhitespace: "", 134 | hasColon: true, 135 | description: [ 136 | .init(" ", "d description") 137 | ] 138 | ), 139 | .init( 140 | relativeLineNumber: 3, 141 | preDashWhitespaces: " ", 142 | keyword: .init(" ", "Parameter"), 143 | name: .init(" ", "c"), 144 | preColonWhitespace: "", 145 | hasColon: true, 146 | description: [ 147 | .init(" ", "c description") 148 | ] 149 | ), 150 | .init( 151 | relativeLineNumber: 4, 152 | preDashWhitespaces: " ", 153 | keyword: .init(" ", "Parameter"), 154 | name: .init(" ", "b"), 155 | preColonWhitespace: "", 156 | hasColon: true, 157 | description: [ 158 | .init(" ", "b description") 159 | ] 160 | ), 161 | .init( 162 | relativeLineNumber: 5, 163 | preDashWhitespaces: " ", 164 | keyword: .init(" ", "Parameter"), 165 | name: .init(" ", "a"), 166 | preColonWhitespace: "", 167 | hasColon: true, 168 | description: [ 169 | .init(" ", "a description"), 170 | .init(" ", "a description continues") 171 | ] 172 | ), 173 | ], 174 | returns: .init( 175 | relativeLineNumber: 7, 176 | preDashWhitespaces: " ", 177 | keyword: .init(" ", "Returns"), 178 | name: .empty, 179 | preColonWhitespace: "", 180 | hasColon: true, 181 | description: [.init(" ", "Returns description")] 182 | ), 183 | throws: .init( 184 | relativeLineNumber: 8, 185 | preDashWhitespaces: " ", 186 | keyword: .init(" ", "Throws"), 187 | name: .empty, 188 | preColonWhitespace: "", 189 | hasColon: true, 190 | description: [.init(" ", "Throws description")] 191 | ) 192 | ) 193 | 194 | let actual = try parse(location: .init(), lines: text.split(separator: "\n").map(String.init)) 195 | 196 | XCTAssertEqual(actual, expected) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/EmptyPatternsTests.swift: -------------------------------------------------------------------------------- 1 | import FileCheck 2 | import DrStringCore 3 | import XCTest 4 | 5 | final class EmptyPatternsTests: XCTestCase { 6 | private let directory: String = { "/" + #file.split(separator: "/").dropLast().joined(separator: "/") }() 7 | 8 | private func runTest(expectation: String, include: [String], exclude: [String], allowEmpty: Bool) -> Bool { 9 | let include = include.map { self.directory + "/Fixtures/" + "\($0).fixture" } 10 | let exclude = exclude.map { self.directory + "/Fixtures/" + "\($0).fixture" } 11 | return fileCheckOutput(against: .buffer(expectation), options: .allowEmptyInput) { 12 | var config = Configuration() 13 | config.includedPaths = include 14 | config.excludedPaths = exclude 15 | config.allowEmptyPatterns = allowEmpty 16 | _ = try? check(with: config, configFile: nil) 17 | } 18 | } 19 | 20 | func testEmptyPatternForExclusionIsReportedWhenItsNotAllowed() { 21 | let expect = """ 22 | // CHECK: Could not find any files matching this 23 | """ 24 | XCTAssert(self.runTest(expectation: expect, include: ["*"], exclude: ["_doesnotexist_/*"], allowEmpty: false)) 25 | } 26 | 27 | func testEmptyPatternForExclusionIsNotReportedWhenItsAllowed() { 28 | let expect = """ 29 | // CHECK-NOT: Could not find any files matching this 30 | """ 31 | XCTAssert(self.runTest(expectation: expect, include: ["*"], exclude: ["_doesnotexist_/*"], allowEmpty: true)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/140.fixture: -------------------------------------------------------------------------------- 1 | // CHECK-NOT: Redundant documentation for `throws` 2 | /// An executable value that can identify issues (violations) in Swift source code. 3 | public protocol Rule { 4 | /// Creates a rule by applying its configuration. 5 | /// 6 | /// - parameter configuration: The untyped configuration value to apply. 7 | /// 8 | /// - throws: Throws if the configuration didn't match the expected format. 9 | init(configuration: Any) throws 10 | } 11 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/147.fixture: -------------------------------------------------------------------------------- 1 | // CHECK-NOT: not properly vertically aligned 2 | /// Check whether the entire input sequence can be matched against the regular expression. 3 | /// 4 | /// - Parameter regex: The regular expression to match against. 5 | /// 6 | /// - Throws: If the regex cannot be created with the given pattern. 7 | /// 8 | /// - Returns: True if str can be matched entirely against regex. 9 | public func matchesEntirely(_ regex: String) throws -> Bool { 10 | return true 11 | } 12 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/Formatting/emptyitem.fixture: -------------------------------------------------------------------------------- 1 | enum init { 2 | /// description 3 | /// 4 | /// - Parameter lineItem: 5 | /// - Returns: 6 | init(_ lineItem: LineItem) -> Int { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/Formatting/emptyitem_expectation.fixture: -------------------------------------------------------------------------------- 1 | enum init { 2 | /// description 3 | /// 4 | /// - Parameter lineItem: 5 | /// - Returns: 6 | init(_ lineItem: LineItem) -> Int { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/Formatting/expectation0.fixture: -------------------------------------------------------------------------------- 1 | /// Description 2 | /// - Parameter foo: foo 3 | /// - Throws: throws stuff 4 | func formattingTest0a(foo: Int) throws {} 5 | 6 | /// Description 7 | /// - Parameter foob: foo 8 | /// - Returns: some string 9 | func formattingTest0b(foob: Int) -> String { 10 | "" 11 | } 12 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/Formatting/expectation1.fixture: -------------------------------------------------------------------------------- 1 | /// Description 2 | /// - Parameter foo: foo 3 | /// - Parameter barz: bar 4 | func formattingTest1a(foo: Int, barz: String) throws {} 5 | 6 | /// Description 7 | /// - Parameter foob: foo 8 | /// - Returns: some string 9 | func formattingTest1b(foob: Int) -> String { "" } 10 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/Formatting/expectation192.fixture: -------------------------------------------------------------------------------- 1 | /// This method will transform any types that are generic 2 | /// 3 | /// - Parameter type: The type to normalize 4 | /// - Parameter replaceSelfWith: If type == "Self"/"Self?"/"Optional" will return this value 5 | /// instead 6 | /// - Parameter stripInOut: Whether or not to strip the inout keyword from the type name 7 | func storedParamType(from type: String, replaceSelfWith: String? = nil, stripInOut: Bool = true) -> String {} 8 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/Formatting/expectation2.fixture: -------------------------------------------------------------------------------- 1 | /// Submits raw data extracted from the driver license, including the barcode string and the 2 | /// photo upload 3 | /// url. 4 | /// 5 | /// - Parameter driverLicenseData: driver license data to submit, which includes the barcode raw 6 | /// data and 7 | /// the url where the photo data 8 | /// was uploaded to. 9 | /// - Parameter completion: completion block. 10 | func submit(driverLicenseData: DriverLicenseData, 11 | completion: @escaping DriverLicenseSubmitCallback) {} 12 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/Formatting/expectation4.fixture: -------------------------------------------------------------------------------- 1 | /// - Returns: The new button mode 2 | public func f() -> B 3 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/Formatting/source0.fixture: -------------------------------------------------------------------------------- 1 | /// Description 2 | ///-Parameters: 3 | ///-foo : foo 4 | /// - throws: throws stuff 5 | func formattingTest0a(foo: Int) throws {} 6 | 7 | /// Description 8 | /// - Parameter foob : foo 9 | ///- Returns: some string 10 | func formattingTest0b(foob: Int) -> String { 11 | "" 12 | } 13 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/Formatting/source1.fixture: -------------------------------------------------------------------------------- 1 | ///Description 2 | ///-parameter foo: foo 3 | ///- Parameter barz: bar 4 | func formattingTest1a(foo: Int, barz: String) throws {} 5 | 6 | ///Description 7 | /// -Parameter foob: foo 8 | /// - returns: some string 9 | func formattingTest1b(foob: Int) -> String { "" } 10 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/Formatting/source192.fixture: -------------------------------------------------------------------------------- 1 | /// This method will transform any types that are generic 2 | /// 3 | /// - Parameters type: The type to normalize 4 | /// - Parameters replaceSelfWith: If type == "Self"/"Self?"/"Optional" will return this value 5 | /// instead 6 | /// - Parameters stripInOut: Whether or not to strip the inout keyword from the type name 7 | func storedParamType(from type: String, replaceSelfWith: String? = nil, stripInOut: Bool = true) -> String {} 8 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/Formatting/source2.fixture: -------------------------------------------------------------------------------- 1 | /// Submits raw data extracted from the driver license, including the barcode string and the 2 | /// photo upload 3 | /// url. 4 | /// 5 | /// - parameter driverLicenseData: driver license data to submit, which includes the barcode raw 6 | /// data and 7 | /// the url where the photo data 8 | /// was uploaded to. 9 | /// - parameter completion: completion block. 10 | func submit(driverLicenseData: DriverLicenseData, 11 | completion: @escaping DriverLicenseSubmitCallback) {} 12 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/Formatting/source4.fixture: -------------------------------------------------------------------------------- 1 | /// - returns: The new button mode 2 | public func f() -> B 3 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/alignAfterColon.fixture: -------------------------------------------------------------------------------- 1 | // CHECK: Line 2 of `foo`'s description is not properly vertically aligned 2 | // CHECK-NOT: Line 3 of `foo`'s description is not properly vertically aligned 3 | /// - Parameter foo: description of foo 4 | /// start of second line 5 | /// start of third line 6 | func f(foo: Int) {} 7 | 8 | // CHECK: Line 2 of `Throws`'s description is not properly vertically aligned 9 | // CHECK-NOT: Line 3 of `foo`'s description is not properly vertically aligned 10 | /// - Throws: description of returns 11 | /// start of second line 12 | /// start of third line 13 | func g() throws {} 14 | 15 | // CHECK: Line 2 of `Returns`'s description is not properly vertically aligned 16 | // CHECK-NOT: Line 3 of `foo`'s description is not properly vertically aligned 17 | /// - Returns: description of returns 18 | /// start of second line 19 | /// start of third line 20 | func h() -> Int {} 21 | 22 | // CHECK: Line 2 of `foo`'s description is not properly vertically aligned 23 | // CHECK-NOT: Line 3 of `foo`'s description is not properly vertically aligned 24 | /// - Parameters: 25 | /// - foo: description of foo 26 | /// start of second line 27 | /// start of third line 28 | func i(foo: Int) {} 29 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/alignAfterColonNotRequired.fixture: -------------------------------------------------------------------------------- 1 | // CHECK: Line 2 of `foo`'s description is not properly vertically aligned 2 | /// - Parameter foo: description of foo 3 | /// start of second line 4 | func f(foo: Int) {} 5 | 6 | // CHECK: Line 2 of `Throws`'s description is not properly vertically aligned 7 | /// - Throws: description of returns 8 | /// start of second line 9 | func g() throws {} 10 | 11 | // CHECK: Line 2 of `Returns`'s description is not properly vertically aligned 12 | /// - Returns: description of returns 13 | /// start of second line 14 | func h() -> Int {} 15 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/async.fixture: -------------------------------------------------------------------------------- 1 | // CHECK-NOT: Redundant documentation for `returns` 2 | /// Builds a protocol graph. 3 | /// 4 | /// - returns: The protocol graph. 5 | func collect() async -> ProtocolGraph { 6 | } 7 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/badParamFormat.fixture: -------------------------------------------------------------------------------- 1 | // CHECK: Parameter `a1` should start with exactly 1 space before `-` 2 | // CHECK: `b1` should have exactly 1 space between `-` and `parameter` 3 | // CHECK: There should be exactly 1 space between `{{(P|p)arameter}}` and `d1` 4 | // CHCEK: For parameter `e1`, there should be no whitespace before `:` 5 | /// - parameter a1: a description 6 | /// - parameter b1: another description 7 | /// - parameterz c1: yet another description 8 | /// - parameter d1: d1 description 9 | /// - parameter e1 : e1 description 10 | func badParamFormat1(a1: Int, b1: Int, c1: Int, d1: Int, e1: Int) { 11 | } 12 | 13 | // CHECK: Parameter `a2` should start with exactly 3 spaces before `-` 14 | // CHCEK: For `e1`, there should be no whitespace before `:` 15 | /// - Parameters: 16 | /// - a2: a description 17 | /// - b2: another description 18 | /// - e1 : e1 description 19 | func badParamFormat2(a2: Int, b2: Int, e1: Int) { 20 | } 21 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/badParametersKeyword.fixture: -------------------------------------------------------------------------------- 1 | // CHECK: `Parameters` should start with exactly 1 space before `-` 2 | /// Descriptions 3 | /// - Parameters: 4 | /// - foo: foo's description 5 | /// - bar: bar's description 6 | func f0(foo: Int, bar: Int) {} 7 | 8 | // CHECK: `parameters` should start with exactly 1 space before `-` 9 | /// Descriptions 10 | /// - parameters: 11 | /// - foo: foo's description 12 | /// - bar: bar's description 13 | func f4(foo: Int, bar: Int) {} 14 | 15 | // CHECK: There should be exactly 1 space between `-` and `Parameters` 16 | /// Descriptions 17 | /// - Parameters: 18 | /// - foo: foo's description 19 | /// - bar: bar's description 20 | func f1(foo: Int, bar: Int) {} 21 | 22 | // CHECK: For `Parameters`, there should be no whitespace before `:` 23 | /// Descriptions 24 | /// - Parameters : 25 | /// - foo: foo's description 26 | /// - bar: bar's description 27 | func f2(foo: Int, bar: Int) {} 28 | 29 | // CHECK: `Parameters` is misspelled as 30 | /// Descriptions 31 | /// - Parametersss: 32 | /// - foo: foo's description 33 | /// - bar: bar's description 34 | func f3(foo: Int, bar: Int) {} 35 | 36 | // CHECK: `:` should be the last character on the line for `Parameters` 37 | /// Descriptions 38 | /// - Parameters: random 39 | /// - foo: foo's description 40 | /// - bar: bar's description 41 | func f5(foo: Int, bar: Int) {} 42 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/badReturnsFormat.fixture: -------------------------------------------------------------------------------- 1 | // CHECK: `returns` should start with exactly 1 space before `-` 2 | /// - returns: returns stuff 3 | func badReturnsFormat1() -> Int { 4 | fatalError() 5 | } 6 | 7 | // CHECK: There should be exactly 1 space between `-` and `returns` 8 | /// - returns: returns stuff 9 | func badReturnsFormat2() -> Int { 10 | fatalError() 11 | } 12 | 13 | // CHECK: For `returns`, there should be no whitespace before `:` 14 | /// - returns : returns stuff 15 | func badReturnsFormat3() -> Int { 16 | fatalError() 17 | } 18 | 19 | // CHECK: For `returns`, there should be exactly 1 space after `:` 20 | /// - returns: returns stuff 21 | func badReturnsFormat4() -> Int { 22 | fatalError() 23 | } 24 | 25 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/badThrowsFormat.fixture: -------------------------------------------------------------------------------- 1 | // CHECK: `throws` should start with exactly 1 space before `-` 2 | /// - throws: throws stuff 3 | func badThrowsFormat1() throws { 4 | } 5 | 6 | // CHECK: There should be exactly 1 space between `-` and `throws` 7 | /// - throws: throws stuff 8 | func badThrowsFormat2() throws { 9 | } 10 | 11 | // CHECK: For `throws`, there should be no whitespace before `:` 12 | /// - throws : throws stuff 13 | func badThrowsFormat3() throws { 14 | } 15 | 16 | // CHECK: For `throws`, there should be exactly 1 space after `:` 17 | /// - throws: throws stuff 18 | func badThrowsFormat4() throws { 19 | } 20 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/complete.fixture: -------------------------------------------------------------------------------- 1 | // CHECK-NOT: docstring problem 2 | /// This is awesome 3 | /// 4 | /// - parameter a: a 5 | /// - parameter b: b 6 | /// 7 | /// - throws: throw this stuff 8 | /// - returns: it's a string 9 | func completelyDocumented(a: String, b: String) throws -> String { 10 | return "" 11 | } 12 | 13 | // CHECK-NOT: docstring problem 14 | /// This is awesome 15 | /// 16 | /// - parameters: 17 | /// - a: a 18 | /// - b: b 19 | /// 20 | /// - throws: throw this stuff 21 | /// - returns: it's a string 22 | func completelyDocumented2(a: String, b: String) throws -> String { 23 | return "" 24 | } 25 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/groupedParameterStyle.fixture: -------------------------------------------------------------------------------- 1 | enum GroupedParameterStyle { 2 | // CHECK: Parameters are organized in the "separate" style, but "grouped" style is preferred 3 | /// Description 4 | /// 5 | /// - Parameter foo: foo description 6 | /// - Parameter bar: bar description 7 | func f(foo: Int, bar: Int) {} 8 | 9 | // CHECK-NOT: Parameters are organized in the "separete" style, but "grouped" style is preferred 10 | /// Description 11 | /// - Parameters: 12 | /// - foo: foo description 13 | /// - bar: bar description 14 | func g(foo: Int, bar: Int) {} 15 | } 16 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/ignoreReturns.fixture: -------------------------------------------------------------------------------- 1 | enum IgnoreReturnsFixture { 2 | // CHECK-NOT: Missing docstring for return type `String` 3 | /// Test0 doc 4 | func test0() -> String { fatalError() } 5 | 6 | // CHECK: `returns` should start with exactly 1 space before `-` 7 | // CHECK: For `returns`, there should be exactly 1 space after `:` 8 | /// Description 9 | /// 10 | /// - parameter t1i0: t1i0 description 11 | ///- returns: description of returns 12 | func test1(_ t1i0: Int) -> String { fatalError() } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/ignoreThrows.fixture: -------------------------------------------------------------------------------- 1 | enum IgnoreThrowsFixture { 2 | // CHECK: Missing docstring for `t0i1` of type `Int` 3 | // CHECK: Unrecognized docstring for `random` 4 | // CHECK-NOT: Missing docstring for throws 5 | // CHECK: Missing docstring for return type `String` 6 | /// Test0 doc 7 | /// 8 | /// - parameter random: random 9 | /// - parameter t0i0: this is t0i0 10 | func test0(_ t0i0: String, lt0i1 t0i1: Int) throws -> String { fatalError() } 11 | 12 | // CHECK: `throws` should start with exactly 1 space before `-` 13 | // CHECK: For `throws`, there should be exactly 1 space after `:` 14 | /// Description 15 | /// 16 | /// - parameter t1i0: t1i0 description 17 | ///- throws: description of throws 18 | func test1(_ t1i0: Int) throws {} 19 | } 20 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/init.fixture: -------------------------------------------------------------------------------- 1 | struct InitTest { 2 | // CHECK: Missing docstring for `x` of type `Int` 3 | /// Description 4 | init(_ x: Int) {} 5 | } 6 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/lowercaseKeywords.fixture: -------------------------------------------------------------------------------- 1 | // CHECK: For `a`, `parameter` is misspelled as 2 | // CHECK: For `b`, `parameter` is misspelled as 3 | // CHECK: `throws` is misspelled as 4 | // CHECK: `returns` is misspelled as 5 | /// function description 6 | /// 7 | /// - parameterz a: a description 8 | /// - Parameter b: b description 9 | /// 10 | /// - Throws: throws description 11 | /// - Returns: returns description 12 | func lowercaseKeywords1(a: Int, b: Int) throws -> Int { 13 | return 0 14 | } 15 | 16 | // CHECK: `throws` is misspelled as 17 | // CHECK: `returns` is misspelled as 18 | /// function description 19 | /// 20 | /// - Parameters: 21 | /// - a: a description 22 | /// - b: b description 23 | /// 24 | /// - Throws: throws description 25 | /// - Returns: returns description 26 | func lowercaseKeywords2(a: Int, b: Int) throws -> Int { 27 | return 0 28 | } 29 | 30 | // CHECK: `throws` is misspelled as 31 | // CHECK: `returns` is misspelled as 32 | /// function description 33 | /// 34 | /// - Parameters: 35 | /// - a: a description 36 | /// - b: b description 37 | /// 38 | /// - Throw: throws description 39 | /// - Return: returns description 40 | func lowercaseKeywords3(a: Int, b: Int) throws -> Int { 41 | return 0 42 | } 43 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/misalignedParameterDescription.fixture: -------------------------------------------------------------------------------- 1 | // CHECK: Line 2 of `short`'s description is not properly vertically aligned 2 | // CHECK-NOT: Line 3 of `short`'s description is not properly vertically aligned 3 | // CHECK: Line 1 of `longer`'s description is not properly vertically aligned 4 | /// function description 5 | /// 6 | /// - parameter short: short description 7 | /// test short description continued 8 | /// test short description continued 9 | /// - parameter longer: longer description 10 | /// - parameter longest: longest description 11 | func f(short: Int, longer: Int, longest: Int) { 12 | } 13 | 14 | // CHECK: Line 2 of `short`'s description is not properly vertically aligned 15 | // CHECK-NOT: Line 3 of `short`'s description is not properly vertically aligned 16 | // CHECK: Line 1 of `longer`'s description is not properly vertically aligned 17 | /// function description 18 | /// 19 | /// - parameters: 20 | /// - short: short description test 21 | /// short description continued 22 | /// test short description continued 23 | /// - longer: longer description 24 | /// - longest: longest description 25 | func g(short: Int, longer: Int, longest: Int) { 26 | } 27 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/missingSectionSeparator.fixture: -------------------------------------------------------------------------------- 1 | /// CHECK: Overall description should end with an empty line 2 | /// CHECK: `b`'s description should end with an empty line 3 | /// CHECK: `throws`'s description should end with an empty line 4 | /// Description 5 | /// Description continues 6 | /// - parameter a: description for a 7 | /// a continued 8 | /// - parameter b: description for b 9 | /// - throws: description for throws 10 | /// - returns: description for returns 11 | func missingSeparatorLines0(a: Int, b: Int) throws -> Int { 0 } 12 | 13 | /// CHECK: Overall description should end with an empty line 14 | /// CHECK: `b`'s description should end with an empty line 15 | /// Description 16 | /// Description continues 17 | /// - Parameters: 18 | /// - a: description for a 19 | /// - b: description for b 20 | /// b continued 21 | /// - Returns: description for returns 22 | func missingSeparatorLines1(a: Int, b: Int) -> Int { 0 } 23 | 24 | /// CHECK: Overall description should end with an empty line 25 | /// Description 26 | /// Description continues 27 | /// - Returns: description for returns 28 | func missingSeparatorLines2() -> Int { 0 } 29 | 30 | /// CHECK-NOT: Overall description should end with an empty line 31 | /// Description 32 | /// Description continues 33 | func missingSeparatorLines3() { } 34 | 35 | /// CHECK: Overall description should end with an empty line 36 | /// CHECK-NOT: `b`'s description should end with an empty line 37 | /// Description 38 | /// Description continues 39 | /// - Parameters: 40 | /// - a: description for a 41 | /// - b: description for b 42 | /// b continued 43 | func missingSeparatorLines4(a: Int, b: Int) {} 44 | 45 | /// CHECK: Overall description should end with an empty line 46 | /// CHECK-NOT: `throws`'s description should end with an empty line 47 | /// Description 48 | /// Description continues 49 | /// - throws: description for throws 50 | func missingSeparatorLines5() throws {} 51 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/missingStuff.fixture: -------------------------------------------------------------------------------- 1 | struct Test0 { 2 | // CHECK: Missing docstring for `t0i1` of type `Int` 3 | // CHECK: Unrecognized docstring for `random` 4 | // CHECK: Missing docstring for throws 5 | // CHECK: Missing docstring for return type `String` 6 | /// Test0 doc 7 | /// 8 | /// - parameter random: random 9 | /// - parameter t0i0: this is t0i0 10 | func test0(_ t0i0: String, lt0i1 t0i1: Int) throws -> String { fatalError() } 11 | } 12 | 13 | // CHECK-NOT: Missing docstring for throws 14 | /// Descriptions 15 | /// - parameters 16 | /// - foo: foo's description 17 | /// - bar: bar's description 18 | func missingStuff(foo: Int, bar: Int) {} 19 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/nodoc.fixture: -------------------------------------------------------------------------------- 1 | // CHECK-NOT: docstring problem 2 | func test0() -> String { fatalError() } 3 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/positional.fixture: -------------------------------------------------------------------------------- 1 | // CHECK: 5:0: warning: |E012| 2 | // CHECK: 6:1: warning: |E002| 3 | // CHECK: 5:18: warning: |E008| 4 | /// f description 5 | /// - Parameter a : a description 6 | func f0(a: Int, b: Int) 7 | 8 | // CHECK: 15:6: warning: |E006| 9 | // CHECK: 15:17: warning: |E019| 10 | // CHECK: 17:10: warning: |E009| 11 | // CHECK: 18:4: warning: |E005| 12 | // CHECK: 18:8: warning: |E007| 13 | /// f description 14 | /// 15 | /// - Parameters 16 | /// - a: description a 17 | /// second line 18 | /// -b: description b 19 | /// - Returns: An int 20 | func f1(a: Int, b: Int) 21 | 22 | // CHECK: 27:9: warning: |E009| 23 | // CHECK: 25:10: warning: |E009| 24 | /// - Returns: some stuff 25 | /// and more 26 | /// - Throws: some stuff 27 | /// and more 28 | func f2() throws -> Int 29 | 30 | // CHECK: 33:0: warning: |E012| 31 | // CHECK: 34:5: warning: |E002| 32 | /// f description 33 | /// - Parameter a: a description 34 | func f3(a: Int, b: Int) 35 | 36 | // CHECK: 39:9: warning: |E004| 37 | /// description 38 | /// 39 | func f4() -> Int 40 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/redundantKeywords.fixture: -------------------------------------------------------------------------------- 1 | // CHECK: Redundant documentation for `throws` 2 | /// description 3 | /// - parameter a: description for a 4 | /// - throws: wat 5 | /// - returns: zero 6 | func redundantKeywords0(a: Int) -> Int { 0 } 7 | 8 | // CHECK: Redundant documentation for `returns` 9 | /// description 10 | /// - parameter a: description for a 11 | /// - throws: wat 12 | /// - returns: zero 13 | func redundantKeywords1(a: Int) throws {} 14 | 15 | // CHECK: Redundant documentation for `throws` 16 | // CHECK: Redundant documentation for `returns` 17 | /// description 18 | /// - parameter a: description for a 19 | /// - throws: wat 20 | /// - returns: zero 21 | func redundantKeywords2(a: Int) {} 22 | 23 | // CHECK: Redundant documentation for `returns` 24 | /// description 25 | /// - parameter a: description for a 26 | /// - returns: zero 27 | func redundantKeywords3(a: Int) {} 28 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/redundantKeywordsPathsOnly.fixture: -------------------------------------------------------------------------------- 1 | // CHECK-NOT: Redundant documentation for `throws` 2 | /// description 3 | /// - parameter a: description for a 4 | /// - throws: wat 5 | /// - returns: zero 6 | func redundantKeywords0(a: Int) -> Int { 0 } 7 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/separateParameterStyle.fixture: -------------------------------------------------------------------------------- 1 | enum SeparateParameterStyle { 2 | // CHECK-NOT: Parameters are organized in the "grouped" style, but "separate" style is preferred. 3 | /// Description 4 | /// 5 | /// - Parameter foo: foo description 6 | /// - Parameter bar: bar description 7 | func f(foo: Int, bar: Int) {} 8 | 9 | // CHECK: Parameters are organized in the "grouped" style, but "separate" style is preferred. 10 | /// Description 11 | /// - Parameters: 12 | /// - foo: foo description 13 | /// - bar: bar description 14 | func g(foo: Int, bar: Int) {} 15 | } 16 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/throwDescriptionNextLine.fixture: -------------------------------------------------------------------------------- 1 | // CHECK-NOT: warning 2 | /// - Throws: 3 | /// - Error1 when x 4 | /// - Error2 when y 5 | func f() throws {} 6 | 7 | // CHECK-NOT: warning 8 | /// - Returns: 9 | /// - 1 when x 10 | /// - 0 when y 11 | func g() -> Int {} 12 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/uppercaseKeywords.fixture: -------------------------------------------------------------------------------- 1 | // CHECK: For `a`, `Parameter` is misspelled as 2 | // CHECK: For `b`, `Parameter` is misspelled as 3 | // CHECK: `Throws` is misspelled as 4 | // CHECK: `Returns` is misspelled as 5 | /// function description 6 | /// 7 | /// - Parameterz a: a description 8 | /// - parameter b: b description 9 | /// 10 | /// - throws: throws description 11 | /// - returns: returns description 12 | func uppercaseKeywords1(a: Int, b: Int) throws -> Int { 13 | return 0 14 | } 15 | 16 | // CHECK: `Throws` is misspelled as 17 | // CHECK: `Returns` is misspelled as 18 | /// function description 19 | /// 20 | /// - parameters: 21 | /// - a: a description 22 | /// - b: b description 23 | /// 24 | /// - throws: throws description 25 | /// - returns: returns description 26 | func uppercaseKeywords2(a: Int, b: Int) throws -> Int { 27 | return 0 28 | } 29 | 30 | // CHECK: `Throws` is misspelled as 31 | // CHECK: `Returns` is misspelled as 32 | /// function description 33 | /// 34 | /// - parameters: 35 | /// - a: a description 36 | /// - b: b description 37 | /// 38 | /// - throw: throws description 39 | /// - return: returns description 40 | func uppercaseKeywords3(a: Int, b: Int) throws -> Int { 41 | return 0 42 | } 43 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/Fixtures/whateverParameterStyle.fixture: -------------------------------------------------------------------------------- 1 | enum WhateverParameterStyle { 2 | // CHECK-NOT: Parameters are organized 3 | /// Description 4 | /// 5 | /// - Parameter foo: foo description 6 | /// - Parameter bar: bar description 7 | func f(foo: Int, bar: Int) {} 8 | 9 | // CHECK-NOT: Parameters are organized 10 | /// Description 11 | /// - Parameters: 12 | /// - foo: foo description 13 | /// - bar: bar description 14 | func g(foo: Int, bar: Int) {} 15 | 16 | // CHECK-NOT: Parameters are organized 17 | /// Description 18 | /// 19 | /// - Parameter foo: foo description 20 | func h(foo: Int) {} 21 | 22 | // CHECK-NOT: Parameters are organized 23 | /// Description 24 | /// 25 | /// - Parameters: 26 | /// - foo: foo description 27 | func i(foo: Int) {} 28 | } 29 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/FormattingTests.swift: -------------------------------------------------------------------------------- 1 | import DrStringCore 2 | import Pathos 3 | import XCTest 4 | 5 | final class FormattingTests: XCTestCase { 6 | private let directory = Path(#file).parent 7 | 8 | func testFormatPatchesFilesProperly0() throws { 9 | try Path.withTemporaryDirectory { path in 10 | for fileName in ["source0", "source1", "expectation0", "expectation1"] { 11 | try! self.directory.joined(with: "Fixtures", "Formatting", "\(fileName).fixture") 12 | .copy(to: path.joined(with: "\(fileName).swift")) 13 | } 14 | 15 | var config = Configuration() 16 | config.includedPaths = ["\(path + "source0.swift")", "\(path + "source1.swift")"] 17 | config.verticalAlignParameterDescription = true 18 | config.parameterStyle = .separate 19 | config.columnLimit = 100 20 | 21 | try format(with: config) 22 | 23 | XCTAssertEqual( 24 | try! (path + "source0.swift").readUTF8String(), 25 | try! (path + "expectation0.swift").readUTF8String() 26 | ) 27 | XCTAssertEqual( 28 | try! (path + "source1.swift").readUTF8String(), 29 | try! (path + "expectation1.swift").readUTF8String() 30 | ) 31 | } 32 | } 33 | 34 | private func verify(sourceName: String, expectationName: String, file: StaticString = #file, line: UInt = #line) throws { 35 | try Path.withTemporaryDirectory { path in 36 | for fileName in [sourceName, expectationName] { 37 | try! self.directory.joined(with: "Fixtures", "Formatting", "\(fileName).fixture") 38 | .copy(to: path + "\(fileName).swift") 39 | } 40 | 41 | var config = Configuration() 42 | config.includedPaths = ["\(path + "\(sourceName).swift")"] 43 | config.verticalAlignParameterDescription = true 44 | config.parameterStyle = .separate 45 | config.columnLimit = 100 46 | try format(with: config) 47 | 48 | XCTAssertEqual( 49 | try! (path + "\(sourceName).swift").readUTF8String(), 50 | try! (path + "\(expectationName).swift").readUTF8String(), 51 | file: file, line: line 52 | ) 53 | } 54 | } 55 | 56 | func testFormatPatchesFilesProperly1() throws { 57 | try self.verify(sourceName: "source2", expectationName: "expectation2", file: #file, line: #line) 58 | } 59 | 60 | func testFormatPatchesFilesProperly2() throws { 61 | try self.verify(sourceName: "source4", expectationName: "expectation4", file: #file, line: #line) 62 | } 63 | 64 | func testFormatHandlesEmptyDocstringItemsCorrectly() throws { 65 | try self.verify(sourceName: "emptyitem", expectationName: "emptyitem_expectation", file: #file, line: #line) 66 | } 67 | 68 | func testBug192() throws { 69 | try self.verify(sourceName: "source192", expectationName: "expectation192", file: #file, line: #line) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/InvalidPatternTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import FileCheck 3 | @testable import DrStringCore 4 | 5 | final class InvalidPatternTests: XCTestCase { 6 | private let directory: String = { "/" + #file.split(separator: "/").dropLast().joined(separator: "/") }() 7 | 8 | private func runTest(expectation: String, include: [String], exclude: [String]) -> Bool { 9 | let include = include.map { self.directory + "/Fixtures/" + "\($0).fixture" } 10 | let exclude = exclude.map { self.directory + "/Fixtures/" + "\($0).fixture" } 11 | return fileCheckOutput(against: .buffer(expectation), options: .allowEmptyInput) { 12 | var config = Configuration() 13 | config.includedPaths = include 14 | config.excludedPaths = exclude 15 | _ = try? check(with: config, configFile: nil) 16 | } 17 | } 18 | 19 | func testInvalidPatternForInclusionIsReported() { 20 | let expect = """ 21 | // CHECK: Could not find any files matching this inclusion pattern 22 | """ 23 | XCTAssert(self.runTest(expectation: expect, include: ["_iDoNotExist_"], exclude: [])) 24 | } 25 | 26 | func testValidPatternForInclusionIsNotReported() { 27 | let expect = """ 28 | // CHECK-NOT: Could not find any files matching this inclusion pattern 29 | """ 30 | XCTAssert(self.runTest(expectation: expect, include: ["complete"], exclude: [])) 31 | } 32 | 33 | func testInvalidPatternForExclusionIsReported() { 34 | let expect = """ 35 | // CHECK: Could not find any files matching this inclusion pattern 36 | """ 37 | XCTAssert(self.runTest(expectation: expect, include: [], exclude: ["_iDoNotExist_"])) 38 | } 39 | 40 | func testValidPatternForExclusionIsNotReported() { 41 | let expect = """ 42 | // CHECK-NOT: Could not find any files matching this 43 | """ 44 | XCTAssert(self.runTest(expectation: expect, include: ["*"], exclude: ["complete"])) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/ProblemCheckingTests.swift: -------------------------------------------------------------------------------- 1 | import DrStringCore 2 | import FileCheck 3 | import XCTest 4 | import Models 5 | 6 | final class ProblemCheckingTests: XCTestCase { 7 | private let directory: String = { "/" + #file.split(separator: "/").dropLast().joined(separator: "/") }() 8 | 9 | private func runTest( 10 | fileName: String, 11 | ignoreThrows: Bool = false, 12 | ignoreReturns: Bool = false, 13 | verticalAlign: Bool = true, 14 | alignAfterColon: [Section] = [], 15 | expectEmpty: Bool = false, 16 | firstLetter: Configuration.FirstKeywordLetterCasing = .uppercase, 17 | needsSeparation: [Section] = [], 18 | parameterStyle: ParameterStyle = .whatever, 19 | format: Configuration.OutputFormat = .plain 20 | ) -> Bool { 21 | let fixture = self.directory + "/Fixtures/" + "\(fileName).fixture" 22 | var config = Configuration() 23 | config.includedPaths = [fixture] 24 | config.ignoreDocstringForThrows = ignoreThrows 25 | config.ignoreDocstringForReturns = ignoreReturns 26 | config.verticalAlignParameterDescription = verticalAlign 27 | config.firstKeywordLetter = firstLetter 28 | config.outputFormat = format 29 | config.separatedSections = needsSeparation 30 | config.parameterStyle = parameterStyle 31 | config.alignAfterColon = alignAfterColon 32 | config.columnLimit = 100 33 | return fileCheckOutput(against: .filePath(fixture), options: expectEmpty ? .allowEmptyInput : []) { 34 | _ = try? check(with: config, configFile: ".drstring.toml") 35 | } 36 | } 37 | 38 | func testCompletelyDocumentedFunction() throws { 39 | XCTAssert(runTest(fileName: "complete", expectEmpty: true, firstLetter: .lowercase)) 40 | } 41 | 42 | func testNoDocNoError() throws { 43 | XCTAssert(runTest(fileName: "nodoc", expectEmpty: true)) 44 | } 45 | 46 | func testMissingStuff() throws { 47 | XCTAssert(runTest(fileName: "missingStuff", firstLetter: .lowercase)) 48 | } 49 | 50 | func testIgnoreThrows() throws { 51 | XCTAssert(runTest(fileName: "ignoreThrows", ignoreThrows: true, firstLetter: .lowercase)) 52 | } 53 | 54 | func testIgnoreReturns() throws { 55 | XCTAssert(runTest(fileName: "ignoreReturns", ignoreReturns: true, firstLetter: .lowercase)) 56 | } 57 | 58 | func testBadParameterFormat() throws { 59 | XCTAssert(runTest(fileName: "badParamFormat", ignoreThrows: true)) 60 | } 61 | 62 | func testBadThrowsFormat() throws { 63 | XCTAssert(runTest(fileName: "badThrowsFormat")) 64 | } 65 | 66 | func testBadReturnsFormat() throws { 67 | XCTAssert(runTest(fileName: "badReturnsFormat")) 68 | } 69 | 70 | func testMisalignedParameterDescriptions() throws { 71 | XCTAssert(runTest(fileName: "misalignedParameterDescription")) 72 | } 73 | 74 | func testLowercaseKeywords() throws { 75 | XCTAssert(runTest(fileName: "lowercaseKeywords", firstLetter: .lowercase)) 76 | } 77 | 78 | func testUppercaseKeywords() throws { 79 | XCTAssert(runTest(fileName: "uppercaseKeywords", firstLetter: .uppercase)) 80 | } 81 | 82 | func testMissingSectionSeparator() throws { 83 | XCTAssert(runTest(fileName: "missingSectionSeparator", expectEmpty: false, 84 | needsSeparation: Section.allCases)) 85 | } 86 | 87 | func testRedundantKeywords() throws { 88 | XCTAssert(runTest(fileName: "redundantKeywords", expectEmpty: false)) 89 | } 90 | 91 | func testRedundantKeywordsPathsOnly() throws { 92 | XCTAssert(runTest(fileName: "redundantKeywordsPathsOnly", expectEmpty: false, format: .paths)) 93 | } 94 | 95 | func testBadParametersKeywordFormat() throws { 96 | XCTAssert(runTest(fileName: "badParametersKeyword", expectEmpty: false, firstLetter: .uppercase)) 97 | } 98 | 99 | func testSeparateParameterStyle() throws { 100 | XCTAssert(runTest(fileName: "separateParameterStyle", expectEmpty: false, parameterStyle: .separate)) 101 | } 102 | 103 | func testGroupedParameterStyle() throws { 104 | XCTAssert(runTest(fileName: "groupedParameterStyle", expectEmpty: false, parameterStyle: .grouped)) 105 | } 106 | 107 | func testWhateverParameterStyle() throws { 108 | XCTAssert(runTest(fileName: "whateverParameterStyle", expectEmpty: true, parameterStyle: .whatever)) 109 | } 110 | 111 | func testAlignAfterColon() throws { 112 | XCTAssert(runTest(fileName: "alignAfterColon", alignAfterColon: [.parameters, .throws, .returns], 113 | expectEmpty: false)) 114 | } 115 | 116 | func testAlignAfterColonNotRequired() throws { 117 | XCTAssert(runTest(fileName: "alignAfterColonNotRequired", 118 | alignAfterColon: [.parameters, .throws, .returns], expectEmpty: true)) 119 | } 120 | 121 | func testInitProblemsAreChecked() throws { 122 | XCTAssert(runTest(fileName: "init")) 123 | } 124 | 125 | func testInitThrowsIsNotRedundant() throws { 126 | XCTAssert(runTest(fileName: "140", ignoreThrows: true)) 127 | } 128 | 129 | func testSeparateLineInThrowsIsNotTreatedAsContinuedBody() throws { 130 | XCTAssert(runTest(fileName: "147", alignAfterColon: [.throws], expectEmpty: true)) 131 | } 132 | 133 | func testProblemPositions() throws { 134 | XCTAssert( 135 | runTest(fileName: "positional", alignAfterColon: [.parameters, .returns, .throws], 136 | needsSeparation: [.description, .parameters])) 137 | } 138 | 139 | func testThrowsWithDescriptionStartingFromNextLine() throws { 140 | XCTAssert(runTest(fileName: "throwDescriptionNextLine", expectEmpty: true)) 141 | } 142 | 143 | func testAsync() throws { 144 | XCTAssert(runTest(fileName: "async")) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Tests/DrStringCoreTests/SuperfluousExclusionTests.swift: -------------------------------------------------------------------------------- 1 | import DrStringCore 2 | import XCTest 3 | import FileCheck 4 | 5 | final class SuperfluousExclusionTests: XCTestCase { 6 | private let directory: String = { "/" + #file.split(separator: "/").dropLast().joined(separator: "/") }() 7 | 8 | private func runTest(expectation: String, include: [String], exclude: [String], allowSuperfluousExclusion: Bool) -> Bool { 9 | let include = include.map { self.directory + "/Fixtures/" + "\($0).fixture" } 10 | let exclude = exclude.map { self.directory + "/Fixtures/" + "\($0).fixture" } 11 | return fileCheckOutput(against: .buffer(expectation), options: .allowEmptyInput) { 12 | var config = Configuration() 13 | config.includedPaths = include 14 | config.excludedPaths = exclude 15 | config.allowSuperfluousExclusion = allowSuperfluousExclusion 16 | config.firstKeywordLetter = .lowercase 17 | config.columnLimit = 100 18 | _ = try? check(with: config, configFile: ".drstring.toml") 19 | } 20 | } 21 | 22 | func testAllowSuperfluousExclusion() { 23 | XCTAssert(runTest( 24 | expectation: "// CHECK-NOT: This file is explicitly excluded in .drstring.toml, but it has no docstring problem", 25 | include: ["complete"], 26 | exclude: ["complete"], 27 | allowSuperfluousExclusion: true) 28 | ) 29 | } 30 | 31 | func testNoSuperfluousExclusion() { 32 | XCTAssert(runTest( 33 | expectation: "// CHECK-NOT: This file is explicitly excluded .drstring.toml, but it has no docstring problem", 34 | include: ["badParamFormat"], 35 | exclude: ["badReturnsFormat"], 36 | allowSuperfluousExclusion: false) 37 | ) 38 | } 39 | 40 | func testNormalExclusionIsNotSuperfluous() { 41 | XCTAssert(runTest( 42 | expectation: "// CHECK-NOT: This file is explicitly excluded .drstring.toml, but it has no docstring problem", 43 | include: ["badParamFormat", "badReturnsFormat"], 44 | exclude: ["badReturnsFormat"], 45 | allowSuperfluousExclusion: false) 46 | ) 47 | } 48 | 49 | func testYesSuperfluousExclusion() { 50 | let expectation = """ 51 | // CHECK: complete 52 | // CHECK: This file is explicitly excluded in .drstring.toml, but it has no docstring problem 53 | """ 54 | 55 | XCTAssert(runTest( 56 | expectation: expectation, 57 | include: ["complete", "badParametersKeyword"], 58 | exclude: ["complete", "badParametersKeyword"], 59 | allowSuperfluousExclusion: false) 60 | ) 61 | } 62 | 63 | func testSuperfluousExclusionViaGlob() { 64 | XCTAssert(runTest( 65 | expectation: "// CHECK-NOT: This file is explicitly excluded in .drstring.toml, but it has no docstring problem", 66 | include: ["badParamFormat"], 67 | exclude: ["*omplete"], 68 | allowSuperfluousExclusion: false) 69 | ) 70 | } 71 | 72 | func testSuperfluousExclusionBecauseItsNotIncludedToBeginWith() { 73 | let expectation = """ 74 | // CHECK: badParamFormat 75 | // CHECK: This file is explicitly excluded, but it's not included for checking anyways 76 | """ 77 | 78 | XCTAssert(runTest( 79 | expectation: expectation, 80 | include: ["badReturnsFormat"], 81 | exclude: ["badParamFormat"], 82 | allowSuperfluousExclusion: false) 83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Tests/EditorTests/DocStringReturnsFormattingTests.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | import XCTest 3 | @testable import Editor 4 | 5 | final class DocStringReturnsFormattingTests: XCTestCase { 6 | func testFormattingBasicReturns() { 7 | let doc = DocString( 8 | location: .init(), 9 | description: [], 10 | parameterHeader: nil, 11 | parameters: [], 12 | returns: .init( 13 | relativeLineNumber: 0, 14 | preDashWhitespaces: " ", 15 | keyword: .init(" ", "Returns"), 16 | name: .init("", ""), 17 | preColonWhitespace: "", 18 | hasColon: true, 19 | description: [ 20 | .init(" ", "description for returns.") 21 | ]), 22 | throws: nil) 23 | 24 | let result = doc.reformat( 25 | initialColumn: 0, 26 | columnLimit: 100, 27 | verticalAlign: false, 28 | alignAfterColon: [], 29 | firstLetterUpperCase: true, 30 | parameterStyle: .whatever, 31 | separations: [] 32 | ) 33 | 34 | XCTAssertEqual( 35 | result, 36 | [ 37 | "/// - Returns: description for returns." 38 | ] 39 | ) 40 | } 41 | 42 | func testFormattingBasicReturnsWithMissingColon() { 43 | let doc = DocString( 44 | location: .init(), 45 | description: [], 46 | parameterHeader: nil, 47 | parameters: [], 48 | returns: .init( 49 | relativeLineNumber: 0, 50 | preDashWhitespaces: " ", 51 | keyword: .init(" ", "Returns"), 52 | name: .init("", ""), 53 | preColonWhitespace: "", 54 | hasColon: false, 55 | description: [ 56 | .init(" ", "description for returns.") 57 | ]), 58 | throws: nil) 59 | 60 | let result = doc.reformat( 61 | initialColumn: 0, 62 | columnLimit: 100, 63 | verticalAlign: false, 64 | alignAfterColon: [], 65 | firstLetterUpperCase: true, 66 | parameterStyle: .whatever, 67 | separations: [] 68 | ) 69 | 70 | XCTAssertEqual( 71 | result, 72 | [ 73 | "/// - Returns: description for returns." 74 | ] 75 | ) 76 | } 77 | 78 | func testFormattingLowercaseKeyword() { 79 | let doc = DocString( 80 | location: .init(), 81 | description: [], 82 | parameterHeader: nil, 83 | parameters: [], 84 | returns: .init( 85 | relativeLineNumber: 0, 86 | preDashWhitespaces: " ", 87 | keyword: .init(" ", "Returns"), 88 | name: .init("", ""), 89 | preColonWhitespace: "", 90 | hasColon: true, 91 | description: [ 92 | .init(" ", "description for returns.") 93 | ]), 94 | throws: nil) 95 | 96 | let result = doc.reformat( 97 | initialColumn: 0, 98 | columnLimit: 100, 99 | verticalAlign: false, 100 | alignAfterColon: [], 101 | firstLetterUpperCase: false, 102 | parameterStyle: .whatever, 103 | separations: [] 104 | ) 105 | 106 | XCTAssertEqual( 107 | result, 108 | [ 109 | "/// - returns: description for returns." 110 | ] 111 | ) 112 | } 113 | 114 | func testFormattingColumnLimitWithAlignAfterColon() { 115 | let doc = DocString( 116 | location: .init(), 117 | description: [], 118 | parameterHeader: nil, 119 | parameters: [], 120 | returns: .init( 121 | relativeLineNumber: 0, 122 | preDashWhitespaces: " ", 123 | keyword: .init(" ", "Returns"), 124 | name: .init("", ""), 125 | preColonWhitespace: "", 126 | hasColon: true, 127 | description: [ 128 | .init(" ", "description for returns.") 129 | ]), 130 | throws: nil) 131 | 132 | let result = doc.reformat( 133 | initialColumn: 8, 134 | columnLimit: 45, 135 | verticalAlign: false, 136 | alignAfterColon: [.returns], 137 | firstLetterUpperCase: true, 138 | parameterStyle: .whatever, 139 | separations: [] 140 | ) 141 | 142 | XCTAssertEqual( 143 | result, 144 | [ 145 | "/// - Returns: description for", 146 | "/// returns." 147 | ] 148 | ) 149 | } 150 | 151 | func testFormattingColumnLimitWithoutAlignAfterColon() { 152 | let doc = DocString( 153 | location: .init(), 154 | description: [], 155 | parameterHeader: nil, 156 | parameters: [], 157 | returns: .init( 158 | relativeLineNumber: 0, 159 | preDashWhitespaces: " ", 160 | keyword: .init(" ", "Returns"), 161 | name: .init("", ""), 162 | preColonWhitespace: "", 163 | hasColon: true, 164 | description: [ 165 | .init(" ", "description for returns.") 166 | ]), 167 | throws: nil) 168 | 169 | let result = doc.reformat( 170 | initialColumn: 8, 171 | columnLimit: 45, 172 | verticalAlign: false, 173 | alignAfterColon: [], 174 | firstLetterUpperCase: true, 175 | parameterStyle: .whatever, 176 | separations: [] 177 | ) 178 | 179 | XCTAssertEqual( 180 | result, 181 | [ 182 | "/// - Returns: description for", 183 | "/// returns." 184 | ] 185 | ) 186 | } 187 | func testFormattingColumnLimitPreservesLeadingWhitespaces() { 188 | let doc = DocString( 189 | location: .init(), 190 | description: [], 191 | parameterHeader: nil, 192 | parameters: [], 193 | returns: .init( 194 | relativeLineNumber: 0, 195 | preDashWhitespaces: " ", 196 | keyword: .init(" ", "Returns"), 197 | name: .init("", ""), 198 | preColonWhitespace: "", 199 | hasColon: true, 200 | description: [ 201 | .init(" ", "description for returns."), 202 | .init(" ", "line 2 description for returns."), 203 | ]), 204 | throws: nil) 205 | 206 | let result = doc.reformat( 207 | initialColumn: 8, 208 | columnLimit: 44, 209 | verticalAlign: false, 210 | alignAfterColon: [], 211 | firstLetterUpperCase: true, 212 | parameterStyle: .whatever, 213 | separations: [] 214 | ) 215 | 216 | XCTAssertEqual( 217 | result, 218 | [ 219 | "/// - Returns: description for", 220 | "/// returns.", 221 | "/// line 2 description for", 222 | "/// returns.", 223 | ] 224 | ) 225 | } 226 | 227 | func testFormattingColumnLimitRemoveExcessLeadingSpaceBeforeColon() { 228 | let doc = DocString( 229 | location: .init(), 230 | description: [], 231 | parameterHeader: nil, 232 | parameters: [], 233 | returns: .init( 234 | relativeLineNumber: 0, 235 | preDashWhitespaces: " ", 236 | keyword: .init(" ", "Returns"), 237 | name: .init("", ""), 238 | preColonWhitespace: "", 239 | hasColon: true, 240 | description: [ 241 | .init(" ", "description for returns."), 242 | .init(" ", "line 2 description for returns."), 243 | ]), 244 | throws: nil) 245 | 246 | let result = doc.reformat( 247 | initialColumn: 8, 248 | columnLimit: 44, 249 | verticalAlign: false, 250 | alignAfterColon: [.returns], 251 | firstLetterUpperCase: true, 252 | parameterStyle: .whatever, 253 | separations: [] 254 | ) 255 | 256 | XCTAssertEqual( 257 | result, 258 | [ 259 | "/// - Returns: description for", 260 | "/// returns.", 261 | "/// line 2 description", 262 | "/// for returns.", 263 | ] 264 | ) 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /Tests/EditorTests/FormatRangeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Models 3 | import Editor 4 | @testable import Crawler 5 | 6 | private let kContent = """ 7 | /// description 8 | func f(a: Int) -> Int { 9 | 10 | } 11 | 12 | /// description 13 | func g() throws -> Int { 14 | 15 | } 16 | """ 17 | 18 | private let kGeneratedFDoc = [ 19 | "/// description", 20 | "/// - Parameter a: <#Int#>", 21 | "/// - Returns: <#Int#>", 22 | ] 23 | 24 | private let kGeneratedGDoc = [ 25 | "/// description", 26 | "/// - Throws: <#Error#>", 27 | "/// - Returns: <#Int#>", 28 | ] 29 | 30 | private extension Documentable { 31 | func formatAt(start: Int?, end: Int?) -> [Edit] { 32 | self.format( 33 | columnLimit: 0, 34 | verticalAlign: false, 35 | alignAfterColon: [], 36 | firstLetterUpperCase: true, 37 | parameterStyle: .whatever, 38 | separations: [], 39 | ignoreThrows: false, 40 | ignoreReturns: false, 41 | addPlaceholder: true, 42 | startLine: start, 43 | endLine: end) 44 | } 45 | } 46 | 47 | final class FormatRangeTests: XCTestCase { 48 | private var functionF: Documentable! 49 | private var functionG: Documentable! 50 | 51 | override func setUp() { 52 | super.setUp() 53 | let documentables = (try? DocExtractor(sourceText: kContent, sourcePath: nil) 54 | .extractDocs()) ?? [] 55 | self.functionF = documentables[0] 56 | self.functionG = documentables[1] 57 | } 58 | 59 | func testAt_0_0() throws { 60 | let result = functionF.formatAt(start: 0, end: 0) 61 | guard result.count == 1 else { 62 | XCTFail("Expected 1 edit") 63 | return 64 | } 65 | 66 | let edit = result[0] 67 | XCTAssertEqual(edit.text, kGeneratedFDoc) 68 | } 69 | 70 | func testAt_0_1() throws { 71 | let result = functionF.formatAt(start: 0, end: 1) 72 | guard result.count == 1 else { 73 | XCTFail("Expected 1 edit") 74 | return 75 | } 76 | 77 | let edit = result[0] 78 | XCTAssertEqual(edit.text, kGeneratedFDoc) 79 | } 80 | 81 | func testAt_0_3() throws { 82 | let result = functionF.formatAt(start: 0, end: 3) 83 | guard result.count == 1 else { 84 | XCTFail("Expected 1 edit") 85 | return 86 | } 87 | 88 | let edit = result[0] 89 | XCTAssertEqual(edit.text, kGeneratedFDoc) 90 | } 91 | 92 | func testAt_0_5() throws { 93 | let fResult = functionF.formatAt(start: 0, end: 5) 94 | guard fResult.count == 1 else { 95 | XCTFail("Expected 1 edit") 96 | return 97 | } 98 | 99 | XCTAssertEqual(fResult[0].text, kGeneratedFDoc) 100 | 101 | let gResult = functionG.formatAt(start: 0, end: 5) 102 | guard gResult.count == 1 else { 103 | XCTFail("Expected 1 edit") 104 | return 105 | } 106 | 107 | XCTAssertEqual(gResult[0].text, kGeneratedGDoc) 108 | } 109 | 110 | func testAt_0_6() throws { 111 | let fResult = functionF.formatAt(start: 0, end: 6) 112 | guard fResult.count == 1 else { 113 | XCTFail("Expected 1 edit") 114 | return 115 | } 116 | 117 | XCTAssertEqual(fResult[0].text, kGeneratedFDoc) 118 | 119 | let gResult = functionG.formatAt(start: 0, end: 6) 120 | guard gResult.count == 1 else { 121 | XCTFail("Expected 1 edit") 122 | return 123 | } 124 | 125 | XCTAssertEqual(gResult[0].text, kGeneratedGDoc) 126 | } 127 | 128 | func testAt_0_8() throws { 129 | let fResult = functionF.formatAt(start: 0, end: 8) 130 | guard fResult.count == 1 else { 131 | XCTFail("Expected 1 edit") 132 | return 133 | } 134 | 135 | XCTAssertEqual(fResult[0].text, kGeneratedFDoc) 136 | 137 | let gResult = functionG.formatAt(start: 0, end: 8) 138 | guard gResult.count == 1 else { 139 | XCTFail("Expected 1 edit") 140 | return 141 | } 142 | 143 | XCTAssertEqual(gResult[0].text, kGeneratedGDoc) 144 | } 145 | 146 | func testAt_5_8() throws { 147 | let fResult = functionF.formatAt(start: 5, end: 8) 148 | XCTAssert(fResult.isEmpty) 149 | 150 | let gResult = functionG.formatAt(start: 5, end: 8) 151 | guard gResult.count == 1 else { 152 | XCTFail("Expected 1 edit") 153 | return 154 | } 155 | 156 | XCTAssertEqual(gResult[0].text, kGeneratedGDoc) 157 | } 158 | 159 | func testAt_8_8() throws { 160 | let fResult = functionF.formatAt(start: 8, end: 8) 161 | XCTAssert(fResult.isEmpty) 162 | 163 | let gResult = functionG.formatAt(start: 8, end: 8) 164 | guard gResult.count == 1 else { 165 | XCTFail("Expected 1 edit") 166 | return 167 | } 168 | 169 | XCTAssertEqual(gResult[0].text, kGeneratedGDoc) 170 | } 171 | 172 | func testAt_4_4() throws { 173 | let fResult = functionF.formatAt(start: 4, end: 4) 174 | XCTAssert(fResult.isEmpty) 175 | 176 | let gResult = functionG.formatAt(start: 4, end: 4) 177 | XCTAssert(gResult.isEmpty) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Tests/EditorTests/LineFoldingTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Editor 2 | import XCTest 3 | 4 | final class LineFoldingTests: XCTestCase { 5 | func testLineWithinLimitRemains() { 6 | let smol = "smol text" 7 | XCTAssertEqual( 8 | fold(line: smol, byLimit: 60), 9 | [smol] 10 | ) 11 | } 12 | 13 | func testLineExceedingLimitFlowsDownward() { 14 | XCTAssertEqual( 15 | fold(line: "test test test test test", byLimit: 10), 16 | [ 17 | "test test", 18 | "test test", 19 | "test", 20 | ] 21 | ) 22 | } 23 | 24 | func testWordExceedingLimitFormItsOwnLine() { 25 | XCTAssertEqual( 26 | fold(line: "test test test_test_test test test", byLimit: 10), 27 | [ 28 | "test test", 29 | "test_test_test", 30 | "test test", 31 | ] 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/EditorTests/ParameterPlaceholderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Editor 3 | import Models 4 | 5 | final class ParameterPlaceholderTests: XCTestCase { 6 | private func formatFunction(rawDoc: [String], parameters: [Parameter]) -> [String] { 7 | Documentable( 8 | path: "", 9 | startLine: 0, 10 | startColumn: 0, 11 | endLine: 0, 12 | name: "f", 13 | docLines: rawDoc, 14 | children: [], 15 | details: .init( 16 | throws: false, 17 | returnType: nil, 18 | parameters: parameters 19 | ) 20 | ).format( 21 | columnLimit: nil, 22 | verticalAlign: false, 23 | alignAfterColon: [], 24 | firstLetterUpperCase: true, 25 | parameterStyle: .separate, 26 | separations: [], 27 | ignoreThrows: false, 28 | ignoreReturns: false, 29 | addPlaceholder: true, 30 | startLine: nil, 31 | endLine: nil 32 | ).first?.text ?? [] 33 | } 34 | 35 | func testNoPlaceholderNecessary() { 36 | let doc = [ 37 | "/// description", 38 | "/// - Parameter a: a", 39 | "/// - Parameter b: b", 40 | ] 41 | 42 | let result = formatFunction( 43 | rawDoc: doc, 44 | parameters: [ 45 | .init(label: nil, name: "a", type: "Int", isVariadic: false, hasDefault: false), 46 | .init(label: nil, name: "b", type: "Int", isVariadic: false, hasDefault: false), 47 | ] 48 | ) 49 | 50 | XCTAssertTrue(result.isEmpty) 51 | } 52 | 53 | func testGeneratePlaceholderAtBeginning() { 54 | let doc = [ 55 | "/// description", 56 | "/// - Parameter b: b", 57 | "/// - Parameter c: c", 58 | ] 59 | 60 | let result = formatFunction( 61 | rawDoc: doc, 62 | parameters: [ 63 | .init(label: nil, name: "a", type: "Int", isVariadic: false, hasDefault: false), 64 | .init(label: nil, name: "b", type: "Int", isVariadic: false, hasDefault: false), 65 | .init(label: nil, name: "c", type: "Int", isVariadic: false, hasDefault: false), 66 | ] 67 | ) 68 | 69 | XCTAssertEqual(result, [ 70 | "/// description", 71 | "/// - Parameter a: <#Int#>", 72 | "/// - Parameter b: b", 73 | "/// - Parameter c: c", 74 | ]) 75 | } 76 | 77 | func testGeneratePlaceholderAtEnd() { 78 | let doc = [ 79 | "/// description", 80 | "/// - Parameter a: a", 81 | "/// - Parameter b: b", 82 | ] 83 | 84 | let result = formatFunction( 85 | rawDoc: doc, 86 | parameters: [ 87 | .init(label: nil, name: "a", type: "Int", isVariadic: false, hasDefault: false), 88 | .init(label: nil, name: "b", type: "Int", isVariadic: false, hasDefault: false), 89 | .init(label: nil, name: "c", type: "Int", isVariadic: false, hasDefault: false), 90 | ] 91 | ) 92 | 93 | XCTAssertEqual(result, [ 94 | "/// description", 95 | "/// - Parameter a: a", 96 | "/// - Parameter b: b", 97 | "/// - Parameter c: <#Int#>", 98 | ]) 99 | } 100 | 101 | func testGeneratePlaceholderAtMiddle() { 102 | let doc = [ 103 | "/// description", 104 | "/// - Parameter a: a", 105 | "/// - Parameter c: c", 106 | ] 107 | 108 | let result = formatFunction( 109 | rawDoc: doc, 110 | parameters: [ 111 | .init(label: nil, name: "a", type: "Int", isVariadic: false, hasDefault: false), 112 | .init(label: nil, name: "b", type: "Int", isVariadic: false, hasDefault: false), 113 | .init(label: nil, name: "c", type: "Int", isVariadic: false, hasDefault: false), 114 | ] 115 | ) 116 | 117 | XCTAssertEqual(result, [ 118 | "/// description", 119 | "/// - Parameter a: a", 120 | "/// - Parameter b: <#Int#>", 121 | "/// - Parameter c: c", 122 | ]) 123 | } 124 | 125 | func testGeneratePlaceholderAtAnywhereButMiddle() { 126 | let doc = [ 127 | "/// description", 128 | "/// - Parameter b: b", 129 | ] 130 | 131 | let result = formatFunction( 132 | rawDoc: doc, 133 | parameters: [ 134 | .init(label: nil, name: "a", type: "Int", isVariadic: false, hasDefault: false), 135 | .init(label: nil, name: "b", type: "Int", isVariadic: false, hasDefault: false), 136 | .init(label: nil, name: "c", type: "Int", isVariadic: false, hasDefault: false), 137 | ] 138 | ) 139 | 140 | XCTAssertEqual(result, [ 141 | "/// description", 142 | "/// - Parameter a: <#Int#>", 143 | "/// - Parameter b: b", 144 | "/// - Parameter c: <#Int#>", 145 | ]) 146 | } 147 | 148 | func testGeneratePlaceholderWhileKeepingRedundantDoc() { 149 | let doc = [ 150 | "/// description", 151 | "/// - Parameter x: x", 152 | "/// - Parameter b: b", 153 | ] 154 | 155 | let result = formatFunction( 156 | rawDoc: doc, 157 | parameters: [ 158 | .init(label: nil, name: "a", type: "Int", isVariadic: false, hasDefault: false), 159 | .init(label: nil, name: "b", type: "Int", isVariadic: false, hasDefault: false), 160 | .init(label: nil, name: "c", type: "Int", isVariadic: false, hasDefault: false), 161 | ] 162 | ) 163 | 164 | XCTAssertEqual(result, [ 165 | "/// description", 166 | "/// - Parameter a: <#Int#>", 167 | "/// - Parameter x: x", 168 | "/// - Parameter b: b", 169 | "/// - Parameter c: <#Int#>", 170 | ]) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Tests/EditorTests/ReturnsPlaceholderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Editor 3 | import Models 4 | 5 | final class ReturnsPlaceholderTests: XCTestCase { 6 | private func formatFunction(rawDoc: [String], returnType: String?, ignore: Bool) -> [String] { 7 | Documentable( 8 | path: "", 9 | startLine: 0, 10 | startColumn: 0, 11 | endLine: 0, 12 | name: "f", 13 | docLines: rawDoc, 14 | children: [], 15 | details: .init( 16 | throws: false, 17 | returnType: returnType, 18 | parameters: [] 19 | ) 20 | ).format( 21 | columnLimit: nil, 22 | verticalAlign: false, 23 | alignAfterColon: [], 24 | firstLetterUpperCase: true, 25 | parameterStyle: .separate, 26 | separations: [], 27 | ignoreThrows: false, 28 | ignoreReturns: ignore, 29 | addPlaceholder: true, 30 | startLine: nil, 31 | endLine: nil 32 | ).first?.text ?? [] 33 | } 34 | 35 | func testGeneratingReturnsNotNecessary() { 36 | let result = formatFunction( 37 | rawDoc: [ 38 | "/// Description", 39 | "/// - Returns: return doc", 40 | ], 41 | returnType: "Int", 42 | ignore: false 43 | ) 44 | 45 | XCTAssertTrue(result.isEmpty) 46 | } 47 | 48 | func testGeneratingReturnsIgnored() { 49 | let doc = [ 50 | "/// Description", 51 | ] 52 | 53 | let result = formatFunction( 54 | rawDoc: doc, 55 | returnType: "Int", 56 | ignore: true 57 | ) 58 | 59 | XCTAssertTrue(result.isEmpty) 60 | } 61 | 62 | func testNotGeneratingReturns() { 63 | let result = formatFunction( 64 | rawDoc: [ 65 | "/// Description", 66 | ], 67 | returnType: nil, 68 | ignore: false 69 | ) 70 | 71 | XCTAssertTrue(result.isEmpty) 72 | } 73 | 74 | func testGeneratingReturns() { 75 | let result = formatFunction( 76 | rawDoc: [ 77 | "/// Description", 78 | ], 79 | returnType: "Int", 80 | ignore: false 81 | ) 82 | 83 | XCTAssertEqual( 84 | result, 85 | [ 86 | "/// Description", 87 | "/// - Returns: <#Int#>" 88 | ] 89 | ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/EditorTests/ThrowsPlaceholderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Editor 3 | import Models 4 | 5 | final class ThrowsPlaceholderTests: XCTestCase { 6 | private func formatFunction(rawDoc: [String], doesThrow: Bool, ignore: Bool) -> [String] { 7 | Documentable( 8 | path: "", 9 | startLine: 0, 10 | startColumn: 0, 11 | endLine: 0, 12 | name: "f", 13 | docLines: rawDoc, 14 | children: [], 15 | details: .init( 16 | throws: doesThrow, 17 | returnType: nil, 18 | parameters: [] 19 | ) 20 | ).format( 21 | columnLimit: nil, 22 | verticalAlign: false, 23 | alignAfterColon: [], 24 | firstLetterUpperCase: true, 25 | parameterStyle: .separate, 26 | separations: [], 27 | ignoreThrows: ignore, 28 | ignoreReturns: false, 29 | addPlaceholder: true, 30 | startLine: nil, 31 | endLine: nil 32 | ).first?.text ?? [] 33 | } 34 | 35 | func testGeneratingThrowsNotNecessary() { 36 | let result = formatFunction( 37 | rawDoc: [ 38 | "/// Description", 39 | "/// - Throws: throw doc", 40 | ], 41 | doesThrow: true, 42 | ignore: false 43 | ) 44 | 45 | XCTAssertTrue(result.isEmpty) 46 | } 47 | 48 | func testGeneratingThrowsIgnored() { 49 | let doc = [ 50 | "/// Description", 51 | ] 52 | 53 | let result = formatFunction( 54 | rawDoc: doc, 55 | doesThrow: true, 56 | ignore: true 57 | ) 58 | 59 | XCTAssertTrue(result.isEmpty) 60 | } 61 | 62 | func testNotGeneratingThrows() { 63 | let result = formatFunction( 64 | rawDoc: [ 65 | "/// Description", 66 | ], 67 | doesThrow: false, 68 | ignore: false 69 | ) 70 | 71 | XCTAssertTrue(result.isEmpty) 72 | } 73 | 74 | func testGeneratingThrows() { 75 | let result = formatFunction( 76 | rawDoc: [ 77 | "/// Description", 78 | ], 79 | doesThrow: true, 80 | ignore: false 81 | ) 82 | 83 | XCTAssertEqual( 84 | result, 85 | [ 86 | "/// Description", 87 | "/// - Throws: <#Error#>" 88 | ] 89 | ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/ModelsTests/LongestCommonSequenceTests.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | import XCTest 3 | 4 | private let kParamA = Parameter(label: nil, name: "a", type: "A", isVariadic: false, hasDefault: false) 5 | private let kParamB = Parameter(label: nil, name: "b", type: "B", isVariadic: false, hasDefault: false) 6 | private let kParamC = Parameter(label: nil, name: "c", type: "C", isVariadic: false, hasDefault: false) 7 | private let kParamD = Parameter(label: nil, name: "d", type: "D", isVariadic: false, hasDefault: false) 8 | 9 | private let kDocParamA = DocString.Entry(relativeLineNumber: 0, preDashWhitespaces: "", keyword: nil, name: .init("", "a"), preColonWhitespace: "", hasColon: true, description: []) 10 | private let kDocParamB = DocString.Entry(relativeLineNumber: .init(), preDashWhitespaces: "", keyword: nil, name: .init("", "b"), preColonWhitespace: "", hasColon: true, description: []) 11 | private let kDocParamC = DocString.Entry(relativeLineNumber: .init(), preDashWhitespaces: "", keyword: nil, name: .init("", "c"), preColonWhitespace: "", hasColon: true, description: []) 12 | private let kDocParamE = DocString.Entry(relativeLineNumber: .init(), preDashWhitespaces: "", keyword: nil, name: .init("", "e"), preColonWhitespace: "", hasColon: true, description: []) 13 | 14 | final class ValidatingTests: XCTestCase { 15 | private func docString(with parameters: [DocString.Entry]) -> DocString { 16 | return DocString( 17 | location: .init(), 18 | description: [], 19 | parameterHeader: nil, 20 | parameters: parameters, 21 | returns: nil, 22 | throws: nil 23 | ) 24 | } 25 | 26 | func testCommonSequenceFindsEmptyForEmptySequence() { 27 | let expected = [Parameter]() 28 | let sig = expected 29 | let doc = self.docString(with: [kDocParamA, kDocParamB, kDocParamC]) 30 | let result = commonSequence(sig, doc) 31 | XCTAssertEqual(result, expected) 32 | } 33 | 34 | func testCommonSequenceFindsStrictSubsequence() { 35 | let expected0 = [kParamA, kParamB, kParamC] 36 | let sig0 = expected0 37 | let doc0 = self.docString(with: [kDocParamA, kDocParamB, kDocParamC]) 38 | let result0 = commonSequence(sig0, doc0) 39 | XCTAssertEqual(result0, expected0) 40 | 41 | let sigParam1 = [kParamA, kParamB, kParamC] 42 | let sig1 = sigParam1 43 | let doc1 = self.docString(with: [kDocParamB, kDocParamC]) 44 | let result1 = commonSequence(sig1, doc1) 45 | XCTAssertEqual(result1, [kParamB, kParamC]) 46 | 47 | let sigParam2 = [kParamA, kParamB, kParamC] 48 | let sig2 = sigParam2 49 | let doc2 = self.docString(with: [kDocParamA, kDocParamB]) 50 | let result2 = commonSequence(sig2, doc2) 51 | XCTAssertEqual(result2, [kParamA, kParamB]) 52 | 53 | let sigParam3 = [kParamA, kParamB, kParamC] 54 | let sig3 = sigParam3 55 | let doc3 = self.docString(with: [kDocParamA, kDocParamC]) 56 | let result3 = commonSequence(sig3, doc3) 57 | XCTAssertEqual(result3, [kParamA, kParamC]) 58 | } 59 | 60 | func testCommonSequenceFindsLongestCommonSubsequence() { 61 | let sigParam = [kParamA, kParamD, kParamB, kParamC] 62 | let docParam = [kDocParamA, kDocParamB, kDocParamE, kDocParamC] 63 | let sig = sigParam 64 | let doc = self.docString(with: docParam) 65 | let result = commonSequence(sig, doc) 66 | XCTAssertEqual(result, [kParamA, kParamB, kParamC]) 67 | } 68 | } 69 | --------------------------------------------------------------------------------