From a105aa90a595ac5b8e2fe3f581a05bb705f5de21 Mon Sep 17 00:00:00 2001 From: w0rp Date: Tue, 20 Jun 2017 10:50:38 +0100 Subject: [PATCH] Fix #668 - Support eslint for TypeScript --- README.md | 2 +- ale_linters/javascript/eslint.vim | 87 +------------------ ale_linters/typescript/eslint.vim | 9 ++ autoload/ale/fix/registry.vim | 2 +- autoload/ale/handlers/eslint.vim | 96 +++++++++++++++++++++ autoload/ale/linter.vim | 20 ++++- doc/ale-typescript.txt | 8 ++ doc/ale.txt | 3 +- test/handler/test_eslint_handler.vader | 35 +++++--- test/test_eslint_executable_detection.vader | 2 +- test/test_linter_retrieval.vader | 19 ++++ 11 files changed, 180 insertions(+), 103 deletions(-) create mode 100644 ale_linters/typescript/eslint.vim diff --git a/README.md b/README.md index a0c702f..2bb0ef0 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ name. That seems to be the fairest way to arrange this table. | Swift | [swiftlint](https://swift.org/) | | Texinfo | [proselint](http://proselint.com/)| | Text^ | [proselint](http://proselint.com/), [vale](https://github.com/ValeLint/vale) | -| TypeScript | [tslint](https://github.com/palantir/tslint), tsserver, typecheck | +| TypeScript | [eslint](http://eslint.org/), [tslint](https://github.com/palantir/tslint), tsserver, typecheck | | Verilog | [iverilog](https://github.com/steveicarus/iverilog), [verilator](http://www.veripool.org/projects/verilator/wiki/Intro) | | Vim | [vint](https://github.com/Kuniwak/vint) | | Vim help^ | [proselint](http://proselint.com/)| diff --git a/ale_linters/javascript/eslint.vim b/ale_linters/javascript/eslint.vim index 9f3bdce..785b8bb 100644 --- a/ale_linters/javascript/eslint.vim +++ b/ale_linters/javascript/eslint.vim @@ -1,92 +1,9 @@ " Author: w0rp " Description: eslint for JavaScript files -let g:ale_javascript_eslint_options = -\ get(g:, 'ale_javascript_eslint_options', '') - -function! ale_linters#javascript#eslint#GetCommand(buffer) abort - let l:executable = ale#handlers#eslint#GetExecutable(a:buffer) - - if ale#Has('win32') && l:executable =~? 'eslint\.js$' - " For Windows, if we detect an eslint.js script, we need to execute - " it with node, or the file can be opened with a text editor. - let l:head = 'node ' . ale#Escape(l:executable) - else - let l:head = ale#Escape(l:executable) - endif - - let l:options = ale#Var(a:buffer, 'javascript_eslint_options') - - return l:head - \ . (!empty(l:options) ? ' ' . l:options : '') - \ . ' -f unix --stdin --stdin-filename %s' -endfunction - -let s:col_end_patterns = [ -\ '\vParsing error: Unexpected token (.+) ', -\ '\v''(.+)'' is not defined.', -\ '\v%(Unexpected|Redundant use of) [''`](.+)[''`]', -\ '\vUnexpected (console) statement', -\] - -function! ale_linters#javascript#eslint#Handle(buffer, lines) abort - let l:config_error_pattern = '\v^ESLint couldn''t find a configuration file' - \ . '|^Cannot read config file' - \ . '|^.*Configuration for rule .* is invalid' - - " Look for a message in the first few lines which indicates that - " a configuration file couldn't be found. - for l:line in a:lines[:10] - if len(matchlist(l:line, l:config_error_pattern)) > 0 - return [{ - \ 'lnum': 1, - \ 'text': 'eslint configuration error (type :ALEDetail for more information)', - \ 'detail': join(a:lines, "\n"), - \}] - endif - endfor - - " Matches patterns line the following: - " - " /path/to/some-filename.js:47:14: Missing trailing comma. [Warning/comma-dangle] - " /path/to/some-filename.js:56:41: Missing semicolon. [Error/semi] - let l:pattern = '^.*:\(\d\+\):\(\d\+\): \(.\+\) \[\(.\+\)\]$' - " This second pattern matches lines like the following: - " - " /path/to/some-filename.js:13:3: Parsing error: Unexpected token - let l:parsing_pattern = '^.*:\(\d\+\):\(\d\+\): \(.\+\)$' - let l:output = [] - - for l:match in ale#util#GetMatches(a:lines, [l:pattern, l:parsing_pattern]) - let l:type = 'Error' - let l:text = l:match[3] - - " Take the error type from the output if available. - if !empty(l:match[4]) - let l:type = split(l:match[4], '/')[0] - let l:text .= ' [' . l:match[4] . ']' - endif - - let l:obj = { - \ 'lnum': l:match[1] + 0, - \ 'col': l:match[2] + 0, - \ 'text': l:text, - \ 'type': l:type ==# 'Warning' ? 'W' : 'E', - \} - - for l:col_match in ale#util#GetMatches(l:text, s:col_end_patterns) - let l:obj.end_col = l:obj.col + len(l:col_match[1]) - 1 - endfor - - call add(l:output, l:obj) - endfor - - return l:output -endfunction - call ale#linter#Define('javascript', { \ 'name': 'eslint', \ 'executable_callback': 'ale#handlers#eslint#GetExecutable', -\ 'command_callback': 'ale_linters#javascript#eslint#GetCommand', -\ 'callback': 'ale_linters#javascript#eslint#Handle', +\ 'command_callback': 'ale#handlers#eslint#GetCommand', +\ 'callback': 'ale#handlers#eslint#Handle', \}) diff --git a/ale_linters/typescript/eslint.vim b/ale_linters/typescript/eslint.vim new file mode 100644 index 0000000..f1ae54e --- /dev/null +++ b/ale_linters/typescript/eslint.vim @@ -0,0 +1,9 @@ +" Author: w0rp +" Description: eslint for JavaScript files + +call ale#linter#Define('typescript', { +\ 'name': 'eslint', +\ 'executable_callback': 'ale#handlers#eslint#GetExecutable', +\ 'command_callback': 'ale#handlers#eslint#GetCommand', +\ 'callback': 'ale#handlers#eslint#Handle', +\}) diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index b1df1c0..05126ff 100644 --- a/autoload/ale/fix/registry.vim +++ b/autoload/ale/fix/registry.vim @@ -14,7 +14,7 @@ let s:default_registry = { \ }, \ 'eslint': { \ 'function': 'ale#fixers#eslint#Fix', -\ 'suggested_filetypes': ['javascript'], +\ 'suggested_filetypes': ['javascript', 'typescript'], \ 'description': 'Apply eslint --fix to a file.', \ }, \ 'isort': { diff --git a/autoload/ale/handlers/eslint.vim b/autoload/ale/handlers/eslint.vim index ac2d936..1c6233d 100644 --- a/autoload/ale/handlers/eslint.vim +++ b/autoload/ale/handlers/eslint.vim @@ -1,6 +1,7 @@ " Author: w0rp " Description: Functions for working with eslint, for checking or fixing files. +call ale#Set('javascript_eslint_options', '') call ale#Set('javascript_eslint_executable', 'eslint') call ale#Set('javascript_eslint_use_global', 0) @@ -11,3 +12,98 @@ function! ale#handlers#eslint#GetExecutable(buffer) abort \ 'node_modules/.bin/eslint', \]) endfunction + +function! ale#handlers#eslint#GetCommand(buffer) abort + let l:executable = ale#handlers#eslint#GetExecutable(a:buffer) + + if ale#Has('win32') && l:executable =~? 'eslint\.js$' + " For Windows, if we detect an eslint.js script, we need to execute + " it with node, or the file can be opened with a text editor. + let l:head = 'node ' . ale#Escape(l:executable) + else + let l:head = ale#Escape(l:executable) + endif + + let l:options = ale#Var(a:buffer, 'javascript_eslint_options') + + return l:head + \ . (!empty(l:options) ? ' ' . l:options : '') + \ . ' -f unix --stdin --stdin-filename %s' +endfunction + +let s:col_end_patterns = [ +\ '\vParsing error: Unexpected token (.+) ', +\ '\v''(.+)'' is not defined.', +\ '\v%(Unexpected|Redundant use of) [''`](.+)[''`]', +\ '\vUnexpected (console) statement', +\] + +function! s:AddHintsForTypeScriptParsingErrors(output) abort + for l:item in a:output + let l:item.text = substitute( + \ l:item.text, + \ '^\(Parsing error\)', + \ '\1 (You may need configure typescript-eslint-parser)', + \ '', + \) + endfor +endfunction + +function! ale#handlers#eslint#Handle(buffer, lines) abort + let l:config_error_pattern = '\v^ESLint couldn''t find a configuration file' + \ . '|^Cannot read config file' + \ . '|^.*Configuration for rule .* is invalid' + + " Look for a message in the first few lines which indicates that + " a configuration file couldn't be found. + for l:line in a:lines[:10] + if len(matchlist(l:line, l:config_error_pattern)) > 0 + return [{ + \ 'lnum': 1, + \ 'text': 'eslint configuration error (type :ALEDetail for more information)', + \ 'detail': join(a:lines, "\n"), + \}] + endif + endfor + + " Matches patterns line the following: + " + " /path/to/some-filename.js:47:14: Missing trailing comma. [Warning/comma-dangle] + " /path/to/some-filename.js:56:41: Missing semicolon. [Error/semi] + let l:pattern = '^.*:\(\d\+\):\(\d\+\): \(.\+\) \[\(.\+\)\]$' + " This second pattern matches lines like the following: + " + " /path/to/some-filename.js:13:3: Parsing error: Unexpected token + let l:parsing_pattern = '^.*:\(\d\+\):\(\d\+\): \(.\+\)$' + let l:output = [] + + for l:match in ale#util#GetMatches(a:lines, [l:pattern, l:parsing_pattern]) + let l:type = 'Error' + let l:text = l:match[3] + + " Take the error type from the output if available. + if !empty(l:match[4]) + let l:type = split(l:match[4], '/')[0] + let l:text .= ' [' . l:match[4] . ']' + endif + + let l:obj = { + \ 'lnum': l:match[1] + 0, + \ 'col': l:match[2] + 0, + \ 'text': l:text, + \ 'type': l:type ==# 'Warning' ? 'W' : 'E', + \} + + for l:col_match in ale#util#GetMatches(l:text, s:col_end_patterns) + let l:obj.end_col = l:obj.col + len(l:col_match[1]) - 1 + endfor + + call add(l:output, l:obj) + endfor + + if expand('#' . a:buffer . ':t') =~? '\.tsx\?$' + call s:AddHintsForTypeScriptParsingErrors(l:output) + endif + + return l:output +endfunction diff --git a/autoload/ale/linter.vim b/autoload/ale/linter.vim index f1d5c09..3c2ddd3 100644 --- a/autoload/ale/linter.vim +++ b/autoload/ale/linter.vim @@ -290,7 +290,7 @@ function! s:GetLinterNames(original_filetype) abort endfunction function! ale#linter#Get(original_filetypes) abort - let l:combined_linters = [] + let l:possibly_duplicated_linters = [] " Handle dot-seperated filetypes. for l:original_filetype in split(a:original_filetypes, '\.') @@ -315,8 +315,22 @@ function! ale#linter#Get(original_filetypes) abort endfor endif - call extend(l:combined_linters, l:filetype_linters) + call extend(l:possibly_duplicated_linters, l:filetype_linters) endfor - return l:combined_linters + let l:name_list = [] + let l:combined_linters = [] + + " Make sure we override linters so we don't get two with the same name, + " like 'eslint' for both 'javascript' and 'typescript' + " + " Note that the reverse calls here modify the List variables. + for l:linter in reverse(l:possibly_duplicated_linters) + if index(l:name_list, l:linter.name) < 0 + call add(l:name_list, l:linter.name) + call add(l:combined_linters, l:linter) + endif + endfor + + return reverse(l:combined_linters) endfunction diff --git a/doc/ale-typescript.txt b/doc/ale-typescript.txt index 009864b..dde3816 100644 --- a/doc/ale-typescript.txt +++ b/doc/ale-typescript.txt @@ -2,6 +2,14 @@ ALE TypeScript Integration *ale-typescript-options* +------------------------------------------------------------------------------- +eslint *ale-typescript-eslint* + +Becauase of how TypeScript compiles code to JavaScript and how interrelated +the two languages are, the `eslint` linter for TypeScript uses the JavaScript +options for `eslint` too. See: |ale-javascript-eslint|. + + ------------------------------------------------------------------------------- tslint *ale-typescript-tslint* diff --git a/doc/ale.txt b/doc/ale.txt index 6a17cc6..9d07a51 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -95,6 +95,7 @@ CONTENTS *ale-contents* chktex..............................|ale-tex-chktex| lacheck.............................|ale-tex-lacheck| typescript............................|ale-typescript-options| + eslint..............................|ale-typescript-eslint| tslint..............................|ale-typescript-tslint| tsserver............................|ale-typescript-tsserver| vim...................................|ale-vim-options| @@ -195,7 +196,7 @@ The following languages and tools are supported. * Swift: 'swiftlint' * Texinfo: 'proselint' * Text: 'proselint', 'vale' -* TypeScript: 'tslint', 'tsserver', 'typecheck' +* TypeScript: 'eslint', 'tslint', 'tsserver', 'typecheck' * Verilog: 'iverilog', 'verilator' * Vim: 'vint' * Vim help: 'proselint' diff --git a/test/handler/test_eslint_handler.vader b/test/handler/test_eslint_handler.vader index 9d5e98f..0a230c7 100644 --- a/test/handler/test_eslint_handler.vader +++ b/test/handler/test_eslint_handler.vader @@ -1,5 +1,5 @@ -Before: - runtime ale_linters/javascript/eslint.vim +After: + unlet! g:config_error_lines Execute(The eslint handler should parse lines correctly): AssertEqual @@ -23,7 +23,7 @@ Execute(The eslint handler should parse lines correctly): \ 'type': 'E', \ }, \ ], - \ ale_linters#javascript#eslint#Handle(347, [ + \ ale#handlers#eslint#Handle(347, [ \ 'This line should be ignored completely', \ '/path/to/some-filename.js:47:14: Missing trailing comma. [Warning/comma-dangle]', \ '/path/to/some-filename.js:56:41: Missing semicolon. [Error/semi]', @@ -51,7 +51,7 @@ Execute(The eslint handler should print a message about a missing configuration \ 'text': 'eslint configuration error (type :ALEDetail for more information)', \ 'detail': join(g:config_error_lines, "\n"), \ }], - \ ale_linters#javascript#eslint#Handle(347, g:config_error_lines[:]) + \ ale#handlers#eslint#Handle(347, g:config_error_lines[:]) Execute(The eslint handler should print a message for config parsing errors): let g:config_error_lines = [ @@ -79,11 +79,7 @@ Execute(The eslint handler should print a message for config parsing errors): \ 'text': 'eslint configuration error (type :ALEDetail for more information)', \ 'detail': join(g:config_error_lines, "\n"), \ }], - \ ale_linters#javascript#eslint#Handle(347, g:config_error_lines[:]) - -After: - unlet! g:config_error_lines - call ale#linter#Reset() + \ ale#handlers#eslint#Handle(347, g:config_error_lines[:]) Execute(The eslint handler should print a message for invalid configuration settings): let g:config_error_lines = [ @@ -113,7 +109,7 @@ Execute(The eslint handler should print a message for invalid configuration sett \ 'text': 'eslint configuration error (type :ALEDetail for more information)', \ 'detail': join(g:config_error_lines, "\n"), \ }], - \ ale_linters#javascript#eslint#Handle(347, g:config_error_lines[:]) + \ ale#handlers#eslint#Handle(347, g:config_error_lines[:]) Execute(The eslint handler should output end_col values where appropriate): AssertEqual @@ -161,7 +157,7 @@ Execute(The eslint handler should output end_col values where appropriate): \ 'type': 'E', \ }, \ ], - \ ale_linters#javascript#eslint#Handle(347, [ + \ ale#handlers#eslint#Handle(347, [ \ 'app.js:4:3: Parsing error: Unexpected token ''some string'' [Error]', \ 'app.js:70:3: ''foo'' is not defined. [Error/no-undef]', \ 'app.js:71:2: Unexpected `await` inside a loop. [Error/no-await-in-loop]', @@ -169,3 +165,20 @@ Execute(The eslint handler should output end_col values where appropriate): \ 'app.js:73:4: Unexpected console statement [Error/no-console]', \ 'app.js:74:4: Unexpected ''debugger'' statement. [Error/no-debugger]', \ ]) + +Execute(The eslint hint about using typescript-eslint-parser): + silent! noautocmd file foo.ts + + AssertEqual + \ [ + \ { + \ 'lnum': 451, + \ 'col': 2, + \ 'end_col': 2, + \ 'text': 'Parsing error (You may need configure typescript-eslint-parser): Unexpected token ) [Error]', + \ 'type': 'E', + \ }, + \ ], + \ ale#handlers#eslint#Handle(bufnr(''), [ + \ 'foo.ts:451:2: Parsing error: Unexpected token ) [Error]', + \ ]) diff --git a/test/test_eslint_executable_detection.vader b/test/test_eslint_executable_detection.vader index c8c4cc1..4f78736 100644 --- a/test/test_eslint_executable_detection.vader +++ b/test/test_eslint_executable_detection.vader @@ -74,4 +74,4 @@ Execute(eslint.js executables should be run with node on Windows): \ 'node ''' \ . g:dir . '/eslint-test-files/react-app/node_modules/eslint/bin/eslint.js' \ . ''' -f unix --stdin --stdin-filename %s', - \ ale_linters#javascript#eslint#GetCommand(bufnr('')) + \ ale#handlers#eslint#GetCommand(bufnr('')) diff --git a/test/test_linter_retrieval.vader b/test/test_linter_retrieval.vader index 480d4f0..d701234 100644 --- a/test/test_linter_retrieval.vader +++ b/test/test_linter_retrieval.vader @@ -106,3 +106,22 @@ Execute (The local alias option shouldn't completely replace the global one): Execute (Linters should be loaded from disk appropriately): AssertEqual [{'name': 'testlinter', 'output_stream': 'stdout', 'executable': 'testlinter', 'command': 'testlinter', 'callback': 'testCB', 'read_buffer': 1, 'lint_file': 0, 'aliases': [], 'lsp': ''}], ale#linter#Get('testft') + + +Execute (Linters for later filetypes should replace the former ones): + call ale#linter#Define('javascript', { + \ 'name': 'eslint', + \ 'executable': 'y', + \ 'command': 'y', + \ 'callback': 'y', + \}) + call ale#linter#Define('typescript', { + \ 'name': 'eslint', + \ 'executable': 'x', + \ 'command': 'x', + \ 'callback': 'x', + \}) + + AssertEqual [ + \ {'output_stream': 'stdout', 'lint_file': 0, 'read_buffer': 1, 'name': 'eslint', 'executable': 'x', 'lsp': '', 'aliases': [], 'command': 'x', 'callback': 'x'} + \], ale#linter#Get('javascript.typescript')