Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hlint #106

Merged
merged 7 commits into from
Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 63 additions & 75 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,26 @@ It uses the same API to report diagnostics as the language server client
built-in to neovim would do. Any customizations you did for
`vim.lsp.diagnostic` apply for this plugin as well.


## Motivation & Goals

With [ale][1] we already got an asynchronous linter, why write yet another one?

Because [ale][1] is a full blown linter including a language server client with
its own commands and functions.


`nvim-lint` is for cases where you use the language server protocol client
built into neovim for 90% of the cases, but you want something to fill the
remaining gaps for languages where there is no good language server
implementation or where the diagnostics reporting of the language server is
inadequate and a better standalone linter exists.


## Installation

- Requires Neovim >= 0.5
- `nvim-lint` is a plugin. Install it like any other Neovim plugin.
- If using [vim-plug][3]: `Plug 'mfussenegger/nvim-lint'`
- If using [packer.nvim][4]: `use 'mfussenegger/nvim-lint'`


## Usage

Configure the linters you want to run per filetype. For example:
Expand All @@ -51,17 +47,15 @@ Some linters require a file to be saved to disk, others support linting `stdin`
input. For such linters you could also define a more aggressive autocmd, for
example on the `InsertLeave` or `TextChanged` events.


## Available Linters

There is a generic linter called `compiler` that uses the `makeprg` and
`errorformat` options of the current buffer.

Other dedicated linters that are built-in are:


| Tool | Linter name |
| ------------------- | -------------- |
| ---------------------------- | -------------- |
| Set via `makeprg` | `compiler` |
| [ansible-lint][ansible-lint] | `ansible_lint` |
| [checkstyle][checkstyle] | `checkstyle` |
Expand All @@ -76,6 +70,7 @@ Other dedicated linters that are built-in are:
| [Flake8][13] | `flake8` |
| [Golangci-lint][16] | `golangcilint` |
| [hadolint][28] | `hadolint` |
| [hlint][32] | `hlint` |
| [HTML Tidy][12] | `tidy` |
| [Inko][17] | `inko` |
| [Languagetool][5] | `languagetool` |
Expand All @@ -94,13 +89,11 @@ Other dedicated linters that are built-in are:
| [vint][21] | `vint` |
| [pycodestyle][pcs-docs] | `pycodestyle` |


## Custom Linters

You can register custom linters by adding them to the `linters` table, but
please consider contributing a linter if it is missing.


```lua
require('lint').linters.your_linter_name = {
cmd = 'linter_cmd',
Expand All @@ -122,7 +115,6 @@ generate some of the properties.
- `output`
- `bufnr`


The `output` is the output generated by the linter command.
The function must return a list of diagnostics as specified in the [language server protocol][9].

Expand All @@ -148,7 +140,6 @@ parser = require('lint.parser').from_errorformat(errorformat)

The function takes a single argument which is the `errorformat`.


### from_pattern

```lua
Expand All @@ -160,13 +151,13 @@ The function allows to parse the linter's output using a lua regex pattern.
- pattern: The regex pattern applied on each line of the output
- groups: The groups specified by the pattern

``` lua
```lua
groups = {"line", "message", "start_col", ["end_col"], ["code"], ["code_desc"], ["file"], ["severity"]}
```

- severity: A mapping from severity codes to diagnostic codes

``` lua
```lua
default_severity = {
['error'] = vim.lsp.protocol.DiagnosticSeverity.Error,
['warning'] = vim.lsp.protocol.DiagnosticSeverity.Warning,
Expand All @@ -177,7 +168,7 @@ default_severity = {

- defaults: The defaults diagnostic values

``` lua
```lua
defaults = {["source"] = "mylint-name"}
```

Expand All @@ -186,66 +177,65 @@ defaults = {["source"] = "mylint-name"}

```typescript
export interface Diagnostic {
/**
* The range at which the message applies.
*/
range: Range;

/**
* The diagnostic's severity. Can be omitted. If omitted it is up to the
* client to interpret diagnostics as error, warning, info or hint.
*/
severity?: DiagnosticSeverity;

/**
* The diagnostic's code, which might appear in the user interface.
*/
code?: integer | string;

/**
* An optional property to describe the error code.
*
* @since 3.16.0
*/
codeDescription?: CodeDescription;

/**
* A human-readable string describing the source of this
* diagnostic, e.g. 'typescript' or 'super lint'.
*/
source?: string;

/**
* The diagnostic's message.
*/
message: string;

/**
* Additional metadata about the diagnostic.
*
* @since 3.15.0
*/
tags?: DiagnosticTag[];

/**
* An array of related diagnostic information, e.g. when symbol-names within
* a scope collide all definitions can be marked via this property.
*/
relatedInformation?: DiagnosticRelatedInformation[];

/**
* A data entry field that is preserved between a
* `textDocument/publishDiagnostics` notification and
* `textDocument/codeAction` request.
*
* @since 3.16.0
*/
data?: unknown;
/**
* The range at which the message applies.
*/
range: Range;

/**
* The diagnostic's severity. Can be omitted. If omitted it is up to the
* client to interpret diagnostics as error, warning, info or hint.
*/
severity?: DiagnosticSeverity;

/**
* The diagnostic's code, which might appear in the user interface.
*/
code?: integer | string;

/**
* An optional property to describe the error code.
*
* @since 3.16.0
*/
codeDescription?: CodeDescription;

/**
* A human-readable string describing the source of this
* diagnostic, e.g. 'typescript' or 'super lint'.
*/
source?: string;

/**
* The diagnostic's message.
*/
message: string;

/**
* Additional metadata about the diagnostic.
*
* @since 3.15.0
*/
tags?: DiagnosticTag[];

/**
* An array of related diagnostic information, e.g. when symbol-names within
* a scope collide all definitions can be marked via this property.
*/
relatedInformation?: DiagnosticRelatedInformation[];

/**
* A data entry field that is preserved between a
* `textDocument/publishDiagnostics` notification and
* `textDocument/codeAction` request.
*
* @since 3.16.0
*/
data?: unknown;
}
```
</details>


</details>

## Alternatives

Expand All @@ -254,13 +244,11 @@ export interface Diagnostic {
- [efm-langserver][6]
- [diagnostic-languageserver][7]


## Development ☢️


### Run tests

Running tests requires [plenary.nvim][plenary] to be checked out in the parent directory of *this* repository.
Running tests requires [plenary.nvim][plenary] to be checked out in the parent directory of _this_ repository.
You can then run:

```bash
Expand All @@ -273,7 +261,6 @@ Or if you want to run a single test file:
nvim --headless --noplugin -u tests/minimal.vim -c "PlenaryBustedDirectory tests/vale_spec.lua {minimal_init = 'tests/minimal.vim'}"
```


[1]: https://github.com/dense-analysis/ale
[3]: https://github.com/junegunn/vim-plug
[4]: https://github.com/wbthomason/packer.nvim
Expand Down Expand Up @@ -304,6 +291,7 @@ nvim --headless --noplugin -u tests/minimal.vim -c "PlenaryBustedDirectory tests
[29]: https://github.com/stylelint/stylelint
[30]: https://github.com/KDE/clazy
[31]: https://github.com/Kampfkarren/selene
[32]: https://github.com/ndmitchell/hlint
[null-ls]: https://github.com/jose-elias-alvarez/null-ls.nvim
[plenary]: https://github.com/nvim-lua/plenary.nvim
[ansible-lint]: https://docs.ansible.com/lint.html
Expand Down
20 changes: 20 additions & 0 deletions lua/lint/linters/hlint.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
return {
cmd = "hlint",
args = { "--json" },
parser = function(output)
local diagnostics = {}
local items = #output > 0 and vim.fn.json_decode(output) or {}
for _, item in ipairs(items) do
table.insert(diagnostics, {
range = {
["start"] = { line = item.startLine, character = item.startColumn },
["end"] = { line = item.endLine, character = item.endColumn },
},
severity = vim.lsp.protocol.DiagnosticSeverity.Error,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output has a severity property - that could be used here to avoid reporting everything as errors.

 hlint --json Foo.hs | jq
[
  {
    "module": [
      "Main"
    ],
    "decl": [
      "foo"
    ],
    "severity": "Warning",
    "hint": "Use concatMap",
    "file": "Foo.hs",
    "startLine": 110,
    "startColumn": 10,
    "endLine": 110,
    "endColumn": 28,
    "from": "concat (map op xs)",
    "to": "concatMap op xs",
    "note": [],

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, missed this. According this there's 3 levels, but they're all in Title case (also evident in your example). Is it gonna be ok?

source = "hlint",
message = item.hint,
})
end
return diagnostics
end,
}
25 changes: 25 additions & 0 deletions tests/hlint_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
describe("linter.hlint", function()
it("can parse the output", function()
local parser = require("lint.linters.hlint").parser
local result = parser([[
[{"module":[],"decl":[],"severity":"Error","hint":"Parse error: possibly incorrect indentation or mismatched brackets","file":"2021-watson/section-1/Main.hs","startLine":3,"startColumn":1,"endLine":3,"endColumn":1,"from":" main = do\n putStrLn (\"1 + 2 = \" ++ show (1 + 2)\n> \n","to":null,"note":[],"refactorings":"[]"}]
]])
assert.are.same(#result, 1)
local expected = {
range = {
["start"] = {
character = 1,
line = 3,
},
["end"] = {
character = 1,
line = 3,
},
},
severity = vim.lsp.protocol.DiagnosticSeverity.Error,
source = "hlint",
message = "Parse error: possibly incorrect indentation or mismatched brackets",
}
assert.are.same(result[1], expected)
end)
end)