dense-analysis / ale

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

Add support for norminette #3910

Closed cassepipe closed 3 years ago

cassepipe commented 3 years ago

Name: norminette URL: https://github.com/42School/norminette

The tuition-free 42 network of schools is using a program to check whether the C code written for the school projects and submitted to peer-review follows a certain style. Its purpose is to enforce a common style that is easily readable by others.

It is easily installed with :

python3 -m pip install --upgrade pip setuptools
python3 -m pip install norminette

Here is an example of what the norminette outputs in the terminal:

src/main.c: Error!                                                              
Error: SPACE_BEFORE_FUNC    (line:   9, col:  12):  space before function name  
Error: ASSIGN_IN_CONTROL    (line:  32, col:  21):  Assignment in control structure
Error: WRONG_SCOPE_COMMENT  (line:  48, col:   9):  Comment is invalid in this scope
Error: TOO_MANY_LINES       (line:  64, col:   1):  Function has more than 25 lines
src/main.h: OK!                                                                    
src/test.c: Error!                                                                 
Error: CONSECUTIVE_NEWLINES (line:   5, col:   1):  Consecutive newlines           
Error: SPACE_REPLACE_TAB    (line:  27, col:   9):  Found space when expecting tab 
libft/ft_strlcpy.c: OK!                                                            
libft/ft_memchr.c: Error!                                                          
Error: TERNARY_FBIDDEN      (line:  26, col:   1):  Ternaries are forbidden        
libft/ft_strlen.c: OK!

Writing a linter for it should be trivial for someone well versed in Vimscript but alas this not my case.

I have not been able to find help to make it work so far so I am asking here in the hope that someone can show me the way forward. Here is my (non-functional) attempt at it :

call ale#Set('c_norminette_executable', 'norminette')
call ale#Set('c_norminette_options', '')

function! ale_linters#c#norminette#GetExecutable(buffer) abort
    return ale#Var(a:buffer, 'c_norminette_executable')
endfunction

function! ale_linters#c#norminette#GetCommand(buffer) abort
    return ale#Escape(ale_linters#c#norminette#GetExecutable(a:buffer))
    \   . ale#Var(a:buffer, 'c_norminette_options')
    \   . ' %t'
endfunction

function! ale_linters#c#norminette#Opscript(buffer, lines) abort
    " Look for lines like the following.
    "
    "ft_lstsize.c: Error!
    "Error: SPACE_REPLACE_TAB    (line:  17, col:  11): Found space when expecting tab
    "ft_calloc.c: OK!
    "ft_memcpy.c: Error!
    "Error: SPACE_AFTER_KW       (line:  22, col:  19): Missing space after keyword
    "test.c: Error!
    "Error: SPACE_BEFORE_FUNC    (line:   6, col:   4): space before function name
    "Error: WRONG_SCOPE_COMMENT  (line:  12, col:   9): Comment is invalid in this scope
    "ft_isalnum.c: OK!

    let l:pattern = '\(^\(\h\+\.[ch]\): \(\w\+\)!$\|^Error: \h\+\s\+(line:\s\+\(\d\+\),\s\+col:\s\+\(\d\+\)):\s\+\(.*\)\)'
    let l:output = []
    let l:curr_file = ''

    for l:match in ale#util#GetMatches(a:lines, l:pattern)
        if l:match[2] == 'OK'
            continue
        elseif l:match[2] == "Error"
            let l:curr_file = l:match[1]
        " if ale#path#IsBufferPath(a:buffer, l:curr_file) && l:match[1] == "Error"
        else
            call add(l:output, {
            \   'lnum': str2nr(l:match[1]),
            \  'col': str2nr(l:match[2]),
            \   'type': 'E',
            \   'text': l:match[3],
            \})
        endif
    endfor

    return l:output
endfunction

call ale#linter#Define('c', {
\   'name': 'norminette',
\   'output_stream': 'both',
\   'executable': function('ale_linters#c#norminette#GetExecutable'),
\   'command': function('ale_linters#c#norminette#GetCommand'),
\   'callback': 'ale_linters#c#norminette#Opscript',
\})

Any help or hint would be much appreciated. Thanks for making ALE, I could write code that fast without it.

hsanson commented 3 years ago

On simple inspection this looks like should work. What problem do you see or what is not working?

cassepipe commented 3 years ago

Well, it does work, I get a >> at the top of the file and I see no error message. I suspect I don't understand very well how the matching function work and/or I get my indexing wrong. I am pretty sure about my pattern though as I tested it on the output thanks to the set incsearch option in Vim. I just don't know how to debug it

hsanson commented 3 years ago

You can run the same commands used in the script inside vim to test the regexp. For example I ran these commands:

:let pattern='\(^\(\h\+\.[ch]\): \(\w\+\)!$\|^Error: \h\+\s\+(line:\s\+\(\d\+\),\s\+col:\s\+\(\d\+\)):\s\+\(.*\)\)'
:echo ale#util#GetMatches(['ft_lstsize.c: Error!'], pattern)

And this was the output:

[['ft_lstsize.c: Error!', 'ft_lstsize.c: Error!', 'ft_lstsize.c', 'Error', '', '', '', '', '', '']]

The regexp works but you are checking l:match[2] == 'OK' or l:match[2] == 'Error' when that value is in l:match[3]. The filename would be in l:match[2] instead of l:match[1]. You can test the other error messages to figure out the correct match index.

Also since we get errors for different files is better to add the filename to the list of errors:

call add(l:output, {
           \    'filename': l:curr_file,
            \   'lnum': str2nr(l:match[1]),
            \  'col': str2nr(l:match[2]),
            \   'type': 'E',
            \   'text': l:match[3],
            \})

Hope this helps you implement the linter. I recommend you submit a PR to make the discussion easier. Do not forget to add documentation, tests, and update the list of supported tools. See :h ale-development for details.

cassepipe commented 3 years ago

Thanks for the debugging tip ! And thanks for pointing out to where the error is and giving advice for betterment. I will try that out ASAP. If I mange to make it work, I'll do the work of documenting it and submit a PR. I don't think I have the level needed to write tests but I will look into that too.