stevearc / conform.nvim

Lightweight yet powerful formatter plugin for Neovim
MIT License
2.55k stars 135 forks source link

bug: incorrect args sequence for ruff format with prepend_args #453

Closed bissakov closed 3 weeks ago

bissakov commented 3 weeks ago

Neovim version (nvim -v)

NVIM v0.10.0 Build type: Release LuaJIT 2.1.1713484068

Operating system/version

Windows 10 Pro N

Add the debug logs

Log file

Log file: D:\Work\ruff_format_bug\.repro\\state\nvim-data\conform.log
          14:38:23[DEBUG] Run command: { "ruff", "--line-length", "80", "format", "--force-exclude", "--stdin-filename", "D:/Work/ruff_format_bug/main.py", "-" }
          14:38:23[DEBUG] Run default CWD: D:\Work\ruff_format_bug
          14:38:23[INFO] ruff_format exited with code 1
          14:38:23[DEBUG] ruff_format stdout: { "main.py:1:5: E999 SyntaxError: Simple statements must be separated by newlines or semicolons", "Found 1 error.", "" }
          14:38:23[DEBUG] ruff_format stderr: { "warning: `ruff <path>` is deprecated. Use `ruff check <path>` instead.", "warning: Ignoring file format in favor of standard input.", "error: Failed to parse main.py:1:5: Simple statements must be separated by newlines or semicolons", "" }
          14:38:23[ERROR] Formatter 'ruff_format' error: warning: `ruff <path>` is deprecated. Use `ruff check <path>` instead.
          warning: Ignoring file format in favor of standard input.
          error: Failed to parse main.py:1:5: Simple statements must be separated by newlines or semicolons

Formatters for this buffer:
ruff_format ready (python) D:\Work\ruff_format_bug\.repro\\data\nvim-data\mason\bin\ruff.CMD

Describe the bug

When specifying the prepend_args property, the final arguments of ruff format are added before format command.

{ "ruff", "--line-length", "80", "format", "--force-exclude", "--stdin-filename", "D:\\Work\\google_ads\\src\\ad.py", "-" }

Should be:

{ "ruff", "format", "--line-length", "80", "--force-exclude", "--stdin-filename", "D:\\Work\\google_ads\\src\\ad.py", "-" }

Or:

{ "ruff", "format", "--force-exclude", "--stdin-filename", "D:\\Work\\google_ads\\src\\ad.py", "-", "--line-length", "80" }

What is the severity of this bug?

tolerable (can work around it)

Steps To Reproduce

  1. nvim -u repro.lua /path/to/test.yaml
  2. Open any Python file.
  3. Try to save the file.
  4. Get the error from BufWritePre autocmd
  5. Open :ConformInfo
  6. See that ruff format argument sequence is incorrect

Expected Behavior

Successful format using ruff format

Minimal example file

Any clean/dirty Python file.


def main():

    a    =1
    b=2

    print (a    +b)

if __name__ == '__main__':
    main()

Minimal init.lua

-- DO NOT change the paths and don't remove the colorscheme
local root = vim.fn.fnamemodify('./.repro', ':p')

-- set stdpaths to use .repro
for _, name in ipairs { 'config', 'data', 'state', 'cache' } do
  vim.env[('XDG_%s_HOME'):format(name:upper())] = root .. '/' .. name
end

-- bootstrap lazy
local lazypath = root .. '/plugins/lazy.nvim'
if not vim.loop.fs_stat(lazypath) then
  vim.fn.system {
    'git',
    'clone',
    '--filter=blob:none',
    '--single-branch',
    'https://github.com/folke/lazy.nvim.git',
    lazypath,
  }
end
vim.opt.runtimepath:prepend(lazypath)

-- install plugins
local plugins = {
  'folke/tokyonight.nvim',
  {
    'hrsh7th/nvim-cmp',
    event = 'InsertEnter',
    opts = function(_, opts)
      opts.sources = opts.sources or {}
      table.insert(opts.sources, {
        name = 'lazydev',
        group_index = 0,
      })
    end,
    dependencies = {
      {
        'L3MON4D3/LuaSnip',
        build = (function()
          if vim.fn.has 'win32' == 1 or vim.fn.executable 'make' == 0 then
            return
          end
          return 'make install_jsregexp'
        end)(),
        dependencies = {},
      },
      'saadparwaiz1/cmp_luasnip',
      'hrsh7th/cmp-nvim-lsp',
      'hrsh7th/cmp-path',
    },
    config = function()
      local cmp = require 'cmp'
      local luasnip = require 'luasnip'
      luasnip.config.setup {}

      cmp.setup {
        snippet = {
          expand = function(args)
            luasnip.lsp_expand(args.body)
          end,
        },
        completion = { completeopt = 'menu,menuone,noinsert' },
        mapping = cmp.mapping.preset.insert {
          ['<C-n>'] = cmp.mapping.select_next_item(),
          ['<C-p>'] = cmp.mapping.select_prev_item(),
          ['<C-b>'] = cmp.mapping.scroll_docs(-4),
          ['<C-f>'] = cmp.mapping.scroll_docs(4),
          ['<C-y>'] = cmp.mapping.confirm { select = true },
          ['<C-Space>'] = cmp.mapping.complete {},

          ['<C-l>'] = cmp.mapping(function()
            if luasnip.expand_or_locally_jumpable() then
              luasnip.expand_or_jump()
            end
          end, { 'i', 's' }),
          ['<C-h>'] = cmp.mapping(function()
            if luasnip.locally_jumpable(-1) then
              luasnip.jump(-1)
            end
          end, { 'i', 's' }),
        },
        sources = {
          { name = 'nvim_lsp' },
          { name = 'luasnip' },
          { name = 'path' },
        },
      }
    end,
  },
  {
    'neovim/nvim-lspconfig',
    dependencies = {
      { 'williamboman/mason.nvim', config = true },
      'williamboman/mason-lspconfig.nvim',
      'WhoIsSethDaniel/mason-tool-installer.nvim',
      { 'j-hui/fidget.nvim', opts = {} },
    },
    config = function()
      vim.api.nvim_create_autocmd('LspAttach', {
        group = vim.api.nvim_create_augroup('kickstart-lsp-attach', { clear = true }),
        callback = function(event)
          local map = function(keys, func, desc)
            vim.keymap.set('n', keys, func, { buffer = event.buf, desc = 'LSP: ' .. desc })
          end
          map('gd', require('telescope.builtin').lsp_definitions, '[G]oto [D]efinition')
          map('gr', require('telescope.builtin').lsp_references, '[G]oto [R]eferences')
          map(
            'gI',
            require('telescope.builtin').lsp_implementations,
            '[G]oto [I]mplementation'
          )
          map(
            '<leader>D',
            require('telescope.builtin').lsp_type_definitions,
            'Type [D]efinition'
          )
          map(
            '<leader>ds',
            require('telescope.builtin').lsp_document_symbols,
            '[D]ocument [S]ymbols'
          )
          map(
            '<leader>ws',
            require('telescope.builtin').lsp_dynamic_workspace_symbols,
            '[W]orkspace [S]ymbols'
          )
          map('<leader>rn', vim.lsp.buf.rename, '[R]e[n]ame')
          map('<leader>ca', vim.lsp.buf.code_action, '[C]ode [A]ction')
          map('gD', vim.lsp.buf.declaration, '[G]oto [D]eclaration')
          local client = vim.lsp.get_client_by_id(event.data.client_id)
          if
            client
            and client.supports_method(vim.lsp.protocol.Methods.textDocument_documentHighlight)
          then
            local highlight_augroup =
              vim.api.nvim_create_augroup('kickstart-lsp-highlight', { clear = false })
            vim.api.nvim_create_autocmd({ 'CursorHold', 'CursorHoldI' }, {
              buffer = event.buf,
              group = highlight_augroup,
              callback = vim.lsp.buf.document_highlight,
            })

            vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, {
              buffer = event.buf,
              group = highlight_augroup,
              callback = vim.lsp.buf.clear_references,
            })

            vim.api.nvim_create_autocmd('LspDetach', {
              group = vim.api.nvim_create_augroup('kickstart-lsp-detach', { clear = true }),
              callback = function(event)
                vim.lsp.buf.clear_references()
                vim.api.nvim_clear_autocmds {
                  group = 'kickstart-lsp-highlight',
                  buffer = event.buf,
                }
              end,
            })
          end

          if
            client
            and client.supports_method(vim.lsp.protocol.Methods.textDocument_inlayHint)
          then
            map('<leader>th', function()
              vim.lsp.inlay_hint.enable(
                not vim.lsp.inlay_hint.is_enabled { bufnr = event.buf }
              )
            end, '[T]oggle Inlay [H]ints')
          end
        end,
      })

      local capabilities = vim.lsp.protocol.make_client_capabilities()
      capabilities = vim.tbl_deep_extend(
        'force',
        capabilities,
        require('cmp_nvim_lsp').default_capabilities()
      )

      local servers = {
        lua_ls = {
          settings = {
            Lua = {
              diagnostics = {
                globals = {
                  'vim',
                  'require',
                },
              },
              completion = {
                callSnippet = 'Replace',
              },
            },
          },
        },
      }

      require('mason').setup()

      local ensure_installed = vim.tbl_keys(servers or {})
      vim.list_extend(ensure_installed, {
        'ruff',
      })
      local excluded_servers = { 'ruff' }
      require('mason-tool-installer').setup { ensure_installed = ensure_installed }

      require('mason-lspconfig').setup {
        handlers = {
          function(server_name)
            if vim.tbl_contains(excluded_servers, server_name) then
              return
            end

            local server = servers[server_name] or {}
            server.capabilities =
              vim.tbl_deep_extend('force', {}, capabilities, server.capabilities or {})

            require('lspconfig')[server_name].setup(server)
          end,
        },
      }
    end,
  },
  {
    'stevearc/conform.nvim',
    event = { 'BufWritePre' },
    cmd = { 'ConformInfo' },
    keys = {
      {
        '<leader>f',
        function()
          require('conform').format {
            async = true,
            lsp_fallback = true,
          }
        end,
        mode = '',
        desc = '[F]ormat buffer',
      },
    },
    config = function()
      require('conform').setup {
        log_level = vim.log.levels.DEBUG,
        notify_on_error = true,
        format_on_save = function(bufnr)
          local disable_filetypes = { c = true, cpp = true }
          return {
            timeout_ms = 500,
            lsp_fallback = not disable_filetypes[vim.bo[bufnr].filetype],
          }
        end,
        format_after_save = {
          lsp_fallback = true,
        },
        formatters_by_ft = {
          lua = { 'stylua' },
          python = {
            'ruff_format',
          },
        },
      }
    end,
  },
}
require('lazy').setup(plugins, {
  root = root .. '/plugins',
})

vim.cmd.colorscheme 'tokyonight'

local conform = require 'conform'
conform.formatters.ruff_format = {
  prepend_args = {
    '--line-length',
    '80',
  },
}

vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = '*',
  callback = function(args)
    conform.format { bufnr = args.buf }
  end,
})

vim.api.nvim_create_autocmd('TextYankPost', {
  desc = 'Highlight when yanking (copying) text',
  group = vim.api.nvim_create_augroup('kickstart-highlight-yank', { clear = true }),
  callback = function()
    vim.highlight.on_yank()
  end,
})

Additional context

The issue can be bypassed by overriding the arguments by putting custom arguments anywhere after format:

conform.formatters.ruff_format = {
  args = {
    'format',
    '--line-length',
    '80',
    '--force-exclude',
    '--stdin-filename',
    '$FILENAME',
    '-',
  },
}

Alternatively, a new property append_args can be added to conform.FormatterConfigOverride:

---@field append_args? string|string[]|fun(self: conform.FormatterConfig, ctx: conform.Context): string|string[]

And in merge_formatter_configs as the functionality is already there:

---@param config conform.FormatterConfig
---@param override conform.FormatterConfigOverride
---@return conform.FormatterConfig
M.merge_formatter_configs = function(config, override)
  local ret = vim.tbl_deep_extend('force', config, override)
  if override.prepend_args then
    M.add_formatter_args(ret, override.prepend_args, { append = false })
  elseif override.append_args then
    M.add_formatter_args(ret, override.append_args, { append = true })
  end
  return ret
end
stevearc commented 3 weeks ago

Added support for append_args