dense-analysis / ale

Check syntax in Vim/Neovim asynchronously and fix files, with Language Server Protocol (LSP) support
BSD 2-Clause "Simplified" License
13.38k stars 1.42k forks source link

Incorrect eslint project root when using nested node_modules #4790

Closed kimberli closed 1 month ago

kimberli commented 1 month ago

Information

VIM version NVIM v0.9.5 Build type: Release

Operating System: Mac OS X 14.3.1 (23D60)

What went wrong

When using eslint in a project that uses pnpm to manage dependencies, the eslint devDependency can be installed into a folder like project/node_modules/.pnpm/eslint@8.57.0/node_modules/eslint/bin/eslint.js.

Reproducing the bug

  1. Make sure pnpm is installed: https://pnpm.io/installation
  2. Clone this repo: https://github.com/kimberli/new-project
  3. Run pnpm install
  4. Open the index.ts file in vim
  5. Expect lint errors to appear for Parsing error (You may need configure typescript-eslint-parser): ',' expected.. Instead you get a Cannot read file '/users/kim/new-project/node_modules/.pnpm/eslint@8.57.0/tsconfig.json'.

:ALEInfo

 Current Filetype: typescript
Available Linters: ['biome', 'cspell', 'deno', 'eslint', 'standard', 'tslint', 'tsserver', 'typecheck', 'xo']
  Enabled Linters: ['eslint']
  Ignored Linters: []
 Suggested Fixers:
  'biome' - Fix JavaScript and TypeScript using biome.
  'deno' - Fix TypeScript using deno fmt.
  'dprint' - Pluggable and configurable code formatting platform
  'eslint' - Apply eslint --fix to a file.
  'prettier' - Apply prettier to a file.
  'remove_trailing_lines' - Remove all blank lines at the end of a file.
  'trim_whitespace' - Remove all trailing whitespace characters at the end of every line.
  'tslint' - Fix typescript files with tslint --fix.
  'xo' - Fix JavaScript/TypeScript files using xo --fix.

 Linter Variables:
" Press Space to read :help for a setting
let g:ale_typescript_eslint_executable = 'eslint_d'
let g:ale_typescript_eslint_use_global = 1

 Global Variables:
" Press Space to read :help for a setting
let g:ale_cache_executable_check_failures = v:null
let g:ale_change_sign_column_color = v:null
let g:ale_command_wrapper = ''
let g:ale_completion_delay = v:null
let g:ale_completion_enabled = 0
let g:ale_completion_max_suggestions = v:null
let g:ale_disable_lsp = 'auto'
let g:ale_echo_cursor = 1
let g:ale_echo_msg_error_str = 'Error'
let g:ale_echo_msg_format = '%code: %%s'
let g:ale_echo_msg_info_str = 'Info'
let g:ale_echo_msg_warning_str = 'Warning'
let g:ale_enabled = 1
let g:ale_fix_on_save = 1
let g:ale_fixers = {'cpp': ['clang-format'], 'typescript': ['eslint'], 'typescriptreact': ['eslint'], 'javascript': ['eslint'], 'typescript.tsx': ['eslint']}
let g:ale_history_enabled = 1
let g:ale_info_default_mode = 'preview'
let g:ale_history_log_output = 1
let g:ale_keep_list_window_open = 0
let g:ale_lint_delay = 200
let g:ale_lint_on_enter = 1
let g:ale_lint_on_filetype_changed = 1
let g:ale_lint_on_insert_leave = 1
let g:ale_lint_on_save = 1
let g:ale_lint_on_text_changed = 'normal'
let g:ale_linter_aliases = {}
let g:ale_linters = {'cpp': ['cpplint'], 'typescript': ['eslint'], 'typescriptreact': ['eslint'], 'javascript': ['eslint'], 'typescript.tsx': ['eslint'], 'python': ['pylint']}
let g:ale_linters_explicit = 0
let g:ale_linters_ignore = {}
let g:ale_list_vertical = 0
let g:ale_list_window_size = 5
let g:ale_loclist_msg_format = '%code: %%s'
let g:ale_max_buffer_history_size = 20
let g:ale_max_signs = v:null
let g:ale_maximum_file_size = v:null
let g:ale_open_list = 1
let g:ale_pattern_options = v:null
let g:ale_pattern_options_enabled = v:null
let g:ale_root = {}
let g:ale_set_balloons = 0
let g:ale_set_highlights = 1
let g:ale_set_loclist = 1
let g:ale_set_quickfix = 0
let g:ale_set_signs = 1
let g:ale_sign_column_always = v:null
let g:ale_sign_error = '>'
let g:ale_sign_info = v:null
let g:ale_sign_offset = v:null
let g:ale_sign_style_error = v:null
let g:ale_sign_style_warning = v:null
let g:ale_sign_warning = '-'
let g:ale_sign_highlight_linenrs = v:null
let g:ale_type_map = {}
let g:ale_use_neovim_diagnostics_api = 1
let g:ale_use_global_executables = v:null
let g:ale_virtualtext_cursor = 'all'
let g:ale_warn_about_trailing_blank_lines = 1
let g:ale_warn_about_trailing_whitespace = 1

  Command History:

(executable check - success) eslint_d
(finished - exit code 1) ['/bin/zsh', '-c', 'cd ''/Users/kim/new-project/node_modules/.pnpm/eslint@8.57.0'' && ''eslint_d'' -f json --stdin --stdin-filename ''/Users/kim/new-project/index.ts'' < ''/var/  folders/b2/z7lztvt5667b9sfxryv2cg_m0000gn/T/nvim.kim/rMvcQ1/1/index.ts''']

<<<OUTPUT STARTS>>>
[{"filePath":"/Users/kim/new-project/index.ts","messages":[{"ruleId":null,"fatal":true,"severity":2,"message":"Parsing error: Cannot read file '/users/kim/new-project/node_modules/.pnpm/eslint@8.57.0/    tsconfig.json'.","nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":1,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"test test\n","usedDeprecatedRules":  []}]
<<<OUTPUT ENDS>>>

Potential fix

In https://github.com/dense-analysis/ale/blob/master/autoload/ale/handlers/eslint.vim:

" Given a buffer, return an appropriate working directory for ESLint.
function! ale#handlers#eslint#GetCwd(buffer) abort
    " ESLint 6 loads plugins/configs/parsers from the project root
    " By default, the project root is simply the CWD of the running process.
    " https://github.com/eslint/rfcs/blob/master/designs/2018-simplified-package-loading/README.md
    " https://github.com/dense-analysis/ale/issues/2787
    "
    " If eslint is installed in a directory which contains the buffer, assume
    " it is the ESLint project root.  Otherwise, use nearest node_modules.
    " Note: If node_modules not present yet, can't load local deps anyway.
    let l:executable = ale#path#FindNearestExecutable(a:buffer, s:executables)

    " Function to truncate path at the first 'node_modules'
    function! TruncateNodeModules(path)
        let l:truncated_path = a:path
        let l:index = stridx(l:truncated_path, 'node_modules')
        if l:index > -1
            let l:truncated_path = l:truncated_path[0:l:index - 1]
        endif
        return l:truncated_path
    endfunction

    if !empty(l:executable)
        let l:modules_root = TruncateNodeModules(l:executable)

        let l:sdks_index = strridx(l:executable, ale#path#Simplify('.yarn/sdks'))
        let l:sdks_root = l:sdks_index > -1 ? l:executable[0:l:sdks_index - 2] : ''
    else
        let l:modules_dir = ale#path#FindNearestDirectory(a:buffer, 'node_modules')
        let l:modules_root = !empty(l:modules_dir) ? TruncateNodeModules(l:modules_dir) : ''

        let l:sdks_dir = ale#path#FindNearestDirectory(a:buffer, ale#path#Simplify('.yarn/sdks'))
        let l:sdks_root = !empty(l:sdks_dir) ? fnamemodify(l:sdks_dir, ':h:h:h') : ''
    endif

    return strlen(l:modules_root) > strlen(l:sdks_root) ? l:modules_root : l:sdks_root
endfunction
kimberli commented 1 month ago

Actually more correct fix is probably https://github.com/dense-analysis/ale/pull/4781 (tested and it seems to do the trick)

w0rp commented 1 month ago

I've merged #4781 now, which should fix this.