From f0887d3e6178482255f11aa378124aef3699245f Mon Sep 17 00:00:00 2001 From: bosr Date: Wed, 7 Apr 2021 12:34:34 +0200 Subject: [PATCH] apple-swift-format: linter and fixer with config swiftpm support (#3671) --- ale_linters/swift/appleswiftformat.vim | 43 +++++++++++++ ale_linters/swift/swiftformat.vim | 62 ------------------- autoload/ale/fix/registry.vim | 5 ++ autoload/ale/fixers/appleswiftformat.vim | 16 +++++ autoload/ale/swift.vim | 57 +++++++++++++++++ doc/ale-swift.txt | 39 ++++++++++++ doc/ale.txt | 1 + ...test_appleswiftformat_fixer_callback.vader | 47 ++++++++++++++ ...er => test_appleswiftformat_handler.vader} | 10 +-- test/linter/test_swift_appleswiftformat.vader | 42 +++++++++++++ test/linter/test_swift_swiftformat.vader | 25 -------- .../.swift-format | 10 +++ .../Package.swift | 0 .../src/folder/dummy.swift | 0 14 files changed, 265 insertions(+), 92 deletions(-) create mode 100644 ale_linters/swift/appleswiftformat.vim delete mode 100644 ale_linters/swift/swiftformat.vim create mode 100644 autoload/ale/fixers/appleswiftformat.vim create mode 100644 test/fixers/test_appleswiftformat_fixer_callback.vader rename test/handler/{test_swiftformat_handler.vader => test_appleswiftformat_handler.vader} (52%) create mode 100644 test/linter/test_swift_appleswiftformat.vader delete mode 100644 test/linter/test_swift_swiftformat.vader create mode 100644 test/test-files/swift/swift-package-project-with-config/.swift-format create mode 100644 test/test-files/swift/swift-package-project-with-config/Package.swift create mode 100644 test/test-files/swift/swift-package-project-with-config/src/folder/dummy.swift diff --git a/ale_linters/swift/appleswiftformat.vim b/ale_linters/swift/appleswiftformat.vim new file mode 100644 index 00000000..4c61764d --- /dev/null +++ b/ale_linters/swift/appleswiftformat.vim @@ -0,0 +1,43 @@ +" Authors: Klaas Pieter Annema , bosr +" Description: Support for swift-format https://github.com/apple/swift-format + +function! ale_linters#swift#appleswiftformat#GetLinterCommand(buffer) abort + let l:command_args = ale#swift#GetAppleSwiftFormatCommand(a:buffer) . ' lint %t' + let l:config_args = ale#swift#GetAppleSwiftFormatConfigArgs(a:buffer) + + if l:config_args isnot# '' + let l:command_args = l:command_args . ' ' . l:config_args + endif + + return l:command_args +endfunction + +function! ale_linters#swift#appleswiftformat#Handle(buffer, lines) abort + " Matches the typical output of swift-format, that is lines of the following pattern: + " + " Sources/main.swift:4:21: warning: [DoNotUseSemicolons] remove ';' and move the next statement to the new line + " Sources/main.swift:3:12: warning: [Spacing] remove 1 space + let l:pattern = '\v^.*:(\d+):(\d+): (\S+): \[(\S+)\] (.*)$' + let l:output = [] + + for l:match in ale#util#GetMatches(a:lines, l:pattern) + call add(l:output, { + \ 'lnum': l:match[1] + 0, + \ 'col': l:match[2] + 0, + \ 'type': l:match[3] is# 'error' ? 'E' : 'W', + \ 'code': l:match[4], + \ 'text': l:match[5], + \}) + endfor + + return l:output +endfunction + +call ale#linter#Define('swift', { +\ 'name': 'apple-swift-format', +\ 'executable': function('ale#swift#GetAppleSwiftFormatExecutable'), +\ 'command': function('ale_linters#swift#appleswiftformat#GetLinterCommand'), +\ 'output_stream': 'stderr', +\ 'language': 'swift', +\ 'callback': 'ale_linters#swift#appleswiftformat#Handle' +\}) diff --git a/ale_linters/swift/swiftformat.vim b/ale_linters/swift/swiftformat.vim deleted file mode 100644 index 2504511a..00000000 --- a/ale_linters/swift/swiftformat.vim +++ /dev/null @@ -1,62 +0,0 @@ -" Author: Klaas Pieter Annema -" Description: Support for swift-format https://github.com/apple/swift-format - -let s:default_executable = 'swift-format' -call ale#Set('swift_swiftformat_executable', s:default_executable) - -function! ale_linters#swift#swiftformat#UseSwift(buffer) abort - let l:swift_config = ale#path#FindNearestFile(a:buffer, 'Package.swift') - let l:executable = ale#Var(a:buffer, 'swift_swiftformat_executable') - - return !empty(l:swift_config) && l:executable is# s:default_executable -endfunction - -function! ale_linters#swift#swiftformat#GetExecutable(buffer) abort - if ale_linters#swift#swiftformat#UseSwift(a:buffer) - return 'swift' - endif - - return ale#Var(a:buffer, 'swift_swiftformat_executable') -endfunction - -function! ale_linters#swift#swiftformat#GetCommand(buffer) abort - let l:executable = ale_linters#swift#swiftformat#GetExecutable(a:buffer) - let l:args = '--mode lint %t' - - if ale_linters#swift#swiftformat#UseSwift(a:buffer) - let l:args = 'run swift-format' . ' ' . l:args - endif - - return ale#Escape(l:executable) . ' ' . l:args -endfunction - -function! ale_linters#swift#swiftformat#Handle(buffer, lines) abort - " Matches lines of the following pattern: - " - " Sources/main.swift:4:21: warning: [DoNotUseSemicolons]: remove ';' and move the next statement to the new line - " Sources/main.swift:3:12: warning: [Spacing]: remove 1 space - let l:pattern = '\v^.*:(\d+):(\d+): (\S+) \[(\S+)\]: (.*)$' - let l:output = [] - - for l:match in ale#util#GetMatches(a:lines, l:pattern) - call add(l:output, { - \ 'lnum': l:match[1] + 0, - \ 'col': l:match[2] + 0, - \ 'type': l:match[3] is# 'error' ? 'E' : 'W', - \ 'code': l:match[4], - \ 'text': l:match[5], - \}) - endfor - - return l:output -endfunction - - -call ale#linter#Define('swift', { -\ 'name': 'swift-format', -\ 'executable': function('ale_linters#swift#swiftformat#GetExecutable'), -\ 'command': function('ale_linters#swift#swiftformat#GetCommand'), -\ 'output_stream': 'stderr', -\ 'language': 'swift', -\ 'callback': 'ale_linters#swift#swiftformat#Handle' -\}) diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index 0cf866dc..c26d4da3 100644 --- a/autoload/ale/fix/registry.vim +++ b/autoload/ale/fix/registry.vim @@ -191,6 +191,11 @@ let s:default_registry = { \ 'suggested_filetypes': ['swift'], \ 'description': 'Apply SwiftFormat to a file.', \ }, +\ 'apple-swift-format': { +\ 'function': 'ale#fixers#appleswiftformat#Fix', +\ 'suggested_filetypes': ['swift'], +\ 'description': 'Apply apple/swift-format to a file.', +\ }, \ 'phpcbf': { \ 'function': 'ale#fixers#phpcbf#Fix', \ 'suggested_filetypes': ['php'], diff --git a/autoload/ale/fixers/appleswiftformat.vim b/autoload/ale/fixers/appleswiftformat.vim new file mode 100644 index 00000000..ca27e82c --- /dev/null +++ b/autoload/ale/fixers/appleswiftformat.vim @@ -0,0 +1,16 @@ +" Author: (bosr) +" Description: Integration of apple/swift-format formatter with ALE. + +function! ale#fixers#appleswiftformat#Fix(buffer) abort + let l:command_args = ale#swift#GetAppleSwiftFormatCommand(a:buffer) . ' format --in-place %t' + let l:config_args = ale#swift#GetAppleSwiftFormatConfigArgs(a:buffer) + + if l:config_args isnot# '' + let l:command_args = l:command_args . ' ' . l:config_args + endif + + return { + \ 'read_temporary_file': 1, + \ 'command': l:command_args, + \} +endfunction diff --git a/autoload/ale/swift.vim b/autoload/ale/swift.vim index b31b8dc5..3232d42a 100644 --- a/autoload/ale/swift.vim +++ b/autoload/ale/swift.vim @@ -11,3 +11,60 @@ function! ale#swift#FindProjectRoot(buffer) abort return '' endfunction + +" Support Apple Swift Format {{{1 + +call ale#Set('swift_appleswiftformat_executable', 'swift-format') +call ale#Set('swift_appleswiftformat_use_swiftpm', 0) + +" Return the executable depending on whether or not to use Swift Package Manager. +" +" If not asked to use Swift Package Manager (use_swiftpm = 0), the returned +" value is the global executable, else the returned value is 'swift' because +" the final command line will be `swift run swift-format ...`. +" +" Failure is expected if use_swiftpm is `1` but no Package.swift can be located. +function! ale#swift#GetAppleSwiftFormatExecutable(buffer) abort + if !ale#Var(a:buffer, 'swift_appleswiftformat_use_swiftpm') + return ale#Var(a:buffer, 'swift_appleswiftformat_executable') + endif + + if ale#path#FindNearestFile(a:buffer, 'Package.swift') is# '' + " If there is no Package.swift file, we don't use swift-format even if it exists, + " so we return '' to indicate failure. + return '' + endif + + return 'swift' +endfunction + +" Return the command depending on whether or not to use Swift Package Manager. +" +" If asked to use Swift Package Manager (use_swiftpm = 1), the command +" arguments are prefixed with 'swift run'. +" +" In either case, the configuration file is located and added to the command. +function! ale#swift#GetAppleSwiftFormatCommand(buffer) abort + let l:executable = ale#swift#GetAppleSwiftFormatExecutable(a:buffer) + let l:command_args = '' + + if ale#Var(a:buffer, 'swift_appleswiftformat_use_swiftpm') + let l:command_args = ' ' . 'run swift-format' + endif + + return ale#Escape(l:executable) . l:command_args +endfunction + +" Locate the nearest '.swift-format' configuration file, and return the +" arguments, else return an empty string. +function! ale#swift#GetAppleSwiftFormatConfigArgs(buffer) abort + let l:config_filepath = ale#path#FindNearestFile(a:buffer, '.swift-format') + + if l:config_filepath isnot# '' + return '--configuration' . ' ' . l:config_filepath + endif + + return '' +endfunction + +" }}} diff --git a/doc/ale-swift.txt b/doc/ale-swift.txt index 8fa0c06c..6d53ca7c 100644 --- a/doc/ale-swift.txt +++ b/doc/ale-swift.txt @@ -2,6 +2,44 @@ ALE Swift Integration *ale-swift-options* +=============================================================================== +apple-swift-format *ale-swift-apple-swift-format* + +There are 3 options to enable linting and fixing with Apple's swift-format: + +1. Install the local executable in your path, as described here: + https://github.com/apple/swift-format +2. Install the executable via your OS package manager, for instance via + Homebrew with `brew install swift-format` +3. Your Swift project has a dependency on the swift-format package, so it can + be run with `swift run swift-format lint ...` In this case, you need to set + a variable, see |g:ale_swift_appleswiftformat_use_swiftpm|. + +Additionally, ALE tries to locate and use the nearest existing `.swift-format` +configuration file. + + +g:ale_swift_appleswiftformat_executable *g:ale_swift_appleswiftformat_executable* + *b:ale_swift_appleswiftformat_executable* + Type: |String| + Default: `'swift-format'` + + This variable can be modified to change the executable path for + `swift-format`. + + +g:ale_swift_appleswiftformat_use_swiftpm *g:ale_swift_appleswiftformat_use_swiftpm* + *b:ale_swift_appleswiftformat_use_swiftpm* + Type: |Number| + Default: `0` + + When set to `1`, this option will cause ALE to use + `swift run swift-format lint ...` instead of the global executable. Use this + option if your Swift project has a dependency on the swift-format package. + + See |ale-integrations-local-executables| + + =============================================================================== sourcekitlsp *ale-swift-sourcekitlsp* @@ -16,6 +54,7 @@ g:ale_sourcekit_lsp_executable *g:ale_sourcekit_lsp_executable* See |ale-integrations-local-executables| + =============================================================================== vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl: diff --git a/doc/ale.txt b/doc/ale.txt index 84d4b096..682db80d 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -3021,6 +3021,7 @@ documented in additional help files. prettier..............................|ale-svelte-prettier| svelteserver..........................|ale-svelte-svelteserver| swift...................................|ale-swift-options| + apple-swift-format....................|ale-swift-apple-swift-format| sourcekitlsp..........................|ale-swift-sourcekitlsp| systemd.................................|ale-systemd-options| systemd-analyze.......................|ale-systemd-analyze| diff --git a/test/fixers/test_appleswiftformat_fixer_callback.vader b/test/fixers/test_appleswiftformat_fixer_callback.vader new file mode 100644 index 00000000..72465f4f --- /dev/null +++ b/test/fixers/test_appleswiftformat_fixer_callback.vader @@ -0,0 +1,47 @@ +Before: + call ale#assert#SetUpFixerTest('swift', 'apple-swift-format') + +After: + call ale#assert#TearDownFixerTest() + +Execute(The swiftformat callback should return the correct default values): + call ale#test#SetFilename('../test-files/swift/dummy.swift') + let g:ale_swift_appleswiftformat_executable = 'xxxinvalid' + let g:ale_swift_appleswiftformat_use_swiftpm = 0 + + AssertEqual + \ { + \ 'read_temporary_file': 1, + \ 'command': ale#Escape(g:ale_swift_appleswiftformat_executable) + \ . ' format --in-place %t', + \ }, + \ ale#fixers#appleswiftformat#Fix(bufnr('')) + +Execute(The swiftformat callback should return the correct default values and located configuration): + call ale#test#SetDirectory('/testplugin/test/test-files/swift/swift-package-project-with-config') + call ale#test#SetFilename('src/folder/dummy.swift') + + let g:ale_swift_appleswiftformat_executable = 'xxxinvalid' + let g:ale_swift_appleswiftformat_use_swiftpm = 0 + + AssertEqual + \ { + \ 'read_temporary_file': 1, + \ 'command': ale#Escape(g:ale_swift_appleswiftformat_executable) + \ . ' format --in-place %t --configuration ' . glob(g:dir . '/.swift-format'), + \ }, + \ ale#fixers#appleswiftformat#Fix(bufnr('')) + + call ale#test#RestoreDirectory() + +Execute(The swiftformat callback should use swiftpm is use_swiftpm is set to 1): + call ale#test#SetFilename('../test-files/swift/swift-package-project/src/folder/dummy.swift') + let g:ale_swift_appleswiftformat_use_swiftpm = 1 + + AssertEqual + \ { + \ 'read_temporary_file': 1, + \ 'command': ale#Escape('swift') + \ . ' run swift-format format --in-place %t', + \ }, + \ ale#fixers#appleswiftformat#Fix(bufnr('')) diff --git a/test/handler/test_swiftformat_handler.vader b/test/handler/test_appleswiftformat_handler.vader similarity index 52% rename from test/handler/test_swiftformat_handler.vader rename to test/handler/test_appleswiftformat_handler.vader index 3dcc4f1a..818bd9c5 100644 --- a/test/handler/test_swiftformat_handler.vader +++ b/test/handler/test_appleswiftformat_handler.vader @@ -1,10 +1,10 @@ Before: - runtime ale_linters/swift/swiftformat.vim + runtime ale_linters/swift/appleswiftformat.vim After: call ale#linter#Reset() -Execute(The swiftformat handler should parse lines correctly): +Execute(The appleswiftformat handler should parse lines correctly): AssertEqual \ [ \ { @@ -22,7 +22,7 @@ Execute(The swiftformat handler should parse lines correctly): \ 'text': 'remove 1 space' \ }, \ ], - \ ale_linters#swift#swiftformat#Handle(bufnr(''), [ - \ 'Sources/main.swift:4:21: warning: [DoNotUseSemicolons]: remove '';'' and move the next statement to the new line', - \ 'Sources/main.swift:3:12: warning: [Spacing]: remove 1 space', + \ ale_linters#swift#appleswiftformat#Handle(bufnr(''), [ + \ 'Sources/main.swift:4:21: warning: [DoNotUseSemicolons] remove '';'' and move the next statement to the new line', + \ 'Sources/main.swift:3:12: warning: [Spacing] remove 1 space', \ ]) diff --git a/test/linter/test_swift_appleswiftformat.vader b/test/linter/test_swift_appleswiftformat.vader new file mode 100644 index 00000000..3dbae8ff --- /dev/null +++ b/test/linter/test_swift_appleswiftformat.vader @@ -0,0 +1,42 @@ +Before: + call ale#assert#SetUpLinterTest('swift', 'appleswiftformat') + +After: + call ale#assert#TearDownLinterTest() + +Execute(Should use default command when use_swiftpm is not set): + call ale#test#SetFilename('../test-files/swift/non-swift-package-project/src/folder/dummy.swift') + + let g:ale_swift_appleswiftformat_executable = 'swift-format' + let g:ale_swift_appleswiftformat_use_swiftpm = 0 + + AssertLinter 'swift-format', ale#Escape('swift-format') . ' lint %t' + +Execute(Should use default command and available configuration when use_swiftpm is not set): + call ale#test#SetDirectory('/testplugin/test/test-files/swift/swift-package-project-with-config') + call ale#test#SetFilename('src/folder/dummy.swift') + + let g:ale_swift_appleswiftformat_executable = 'swift-format' + let g:ale_swift_appleswiftformat_use_swiftpm = 0 + + AssertLinter 'swift-format', + \ ale#Escape('swift-format') . ' lint %t ' . '--configuration ' + \ . glob(g:dir . '/.swift-format') + + call ale#test#RestoreDirectory() + +Execute(Should use swift run when use_swiftpm is set to 1): + call ale#test#SetFilename('../test-files/swift/swift-package-project/src/folder/dummy.swift') + + let g:ale_swift_appleswiftformat_use_swiftpm = 1 + + AssertLinter 'swift', ale#Escape('swift') . ' run swift-format lint %t' + +Execute(Should use the provided global executable): + call ale#test#SetFilename('../test-files/swift/swift-package-project/src/folder/dummy.swift') + + let g:ale_swift_appleswiftformat_executable = '/path/to/custom/swift-format' + let g:ale_swift_appleswiftformat_use_swiftpm = 0 + + AssertLinter '/path/to/custom/swift-format', + \ ale#Escape('/path/to/custom/swift-format') . ' lint %t' diff --git a/test/linter/test_swift_swiftformat.vader b/test/linter/test_swift_swiftformat.vader deleted file mode 100644 index 9f6ee62e..00000000 --- a/test/linter/test_swift_swiftformat.vader +++ /dev/null @@ -1,25 +0,0 @@ -Before: - call ale#assert#SetUpLinterTest('swift', 'swiftformat') - -After: - call ale#assert#TearDownLinterTest() - -Execute(Should use default command when not in a swift package): - call ale#test#SetFilename('../test-files/swift/non-swift-package-project/src/folder/dummy.swift') - - AssertLinter 'swift-format', - \ ale#Escape('swift-format') . ' --mode lint %t' - -Execute(Should use swift run when in a swift package): - call ale#test#SetFilename('../test-files/swift/swift-package-project/src/folder/dummy.swift') - - AssertLinter 'swift', - \ ale#Escape('swift') . ' run swift-format --mode lint %t' - -Execute(Should let users configure a global executable and override local paths): - call ale#test#SetFilename('../test-files/swift/swift-package-project/src/folder/dummy.swift') - - let g:ale_swift_swiftformat_executable = '/path/to/custom/swift-format' - - AssertLinter '/path/to/custom/swift-format', - \ ale#Escape('/path/to/custom/swift-format') . ' --mode lint %t' diff --git a/test/test-files/swift/swift-package-project-with-config/.swift-format b/test/test-files/swift/swift-package-project-with-config/.swift-format new file mode 100644 index 00000000..19fb8b96 --- /dev/null +++ b/test/test-files/swift/swift-package-project-with-config/.swift-format @@ -0,0 +1,10 @@ +{ + "version": 1, + "lineLength": 100, + "indentation": { + "spaces": 4 + }, + "respectsExistingLineBreaks": true, + "lineBreakBeforeControlFlowKeywords": true, + "lineBreakBeforeEachArgument": true +} diff --git a/test/test-files/swift/swift-package-project-with-config/Package.swift b/test/test-files/swift/swift-package-project-with-config/Package.swift new file mode 100644 index 00000000..e69de29b diff --git a/test/test-files/swift/swift-package-project-with-config/src/folder/dummy.swift b/test/test-files/swift/swift-package-project-with-config/src/folder/dummy.swift new file mode 100644 index 00000000..e69de29b