1

I am trying to write an ale linter for the norminette program. How can I make it work ?

It's a program that checks if C source code files follow the coding norm of the 42 school network and it would save me time to be able to have it integrate tis he ale plugin.

I am not well versed in VimL and this is what I was able to come up with, putting this in the ~/.vim/plugged/ale/ale_linters/c directory :

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',
\})

There's an comment in the function that shows what the norminette ouputs to the terminal. I know that my pattern works because I tested it on the output thanks to the set incsearch parameter.

" Given a single line, or a List of lines, and a single pattern, or a List      
" of patterns, return all of the matches for the lines(s) from the given        
" patterns, using matchlist().                                                  
"                                                                               
" Only the first pattern which matches a line will be returned.                 
function! ale#util#GetMatches(lines, patterns) abort                            
    let l:matches = []                                                          
    let l:lines = type(a:lines) is v:t_list ? a:lines : [a:lines]               
    let l:patterns = type(a:patterns) is v:t_list ? a:patterns : [a:patterns]   
                                                                                
    for l:line in l:lines                                                       
        for l:pattern in l:patterns                                             
            let l:match = matchlist(l:line, l:pattern)                          
                                                                                
            if !empty(l:match)                                                  
                call add(l:matches, l:match)                                    
                break                                                           
            endif                                                               
        endfor                                                                  
    endfor                                                                      
                                                                                
    return l:matches                                                            
endfunction

If you want to see it for yourself and install it, it as easy as running:

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

Here is the ale_linters#c#norminette#GetExecutable function :

" Given a single line, or a List of lines, and a single pattern, or a List      
" of patterns, return all of the matches for the lines(s) from the given        
" patterns, using matchlist().                                                  
"                                                                               
" Only the first pattern which matches a line will be returned.                 
function! ale#util#GetMatches(lines, patterns) abort                            
    let l:matches = []                                                          
    let l:lines = type(a:lines) is v:t_list ? a:lines : [a:lines]               
    let l:patterns = type(a:patterns) is v:t_list ? a:patterns : [a:patterns]   
                                                                                
    for l:line in l:lines                                                       
        for l:pattern in l:patterns                                             
            let l:match = matchlist(l:line, l:pattern)                          
                                                                                
            if !empty(l:match)                                                  
                call add(l:matches, l:match)                                    
                break                                                           
            endif                                                               
        endfor                                                                  
    endfor                                                                      
                                                                                
    return l:matches                                                            
endfunction

I am sorry to ask such a specific question. I've given it my best and I've tried other channels for help but I have not found any. This is my last attempt before I actually buy a book to learn how to program in Vimscript :) (I am getting quite good at vimregex though)

5
  • Apparently there is no syntax highlighting for viml on SO. Sorry about that.
    – cassepipe
    Sep 17 at 17:08
  • What exactly is the question here? Is something not working? Fwiw, I would approach this by scanning for lines matching filename: Error and then looking at the lines after that for the messages and such (nested loops, two patterns instead of one).
    – D. Ben Knoble
    Sep 18 at 10:57
  • 1
    @D.BenKnoble Edited to ask my "question" that is how do I make it work ? Thanks for your advice, I'll see if I can go further with it.
    – cassepipe
    Sep 18 at 14:46
  • I've rolled back your edit. Questions should stay questions, even if you now have answers for them. Please self-answer instead—this is encouraged here! I'm glad you figured it out; I was coming back to write an answer when I saw you had solved your problem. (If you need the content of the old edit, you can get it in the edit history.)
    – D. Ben Knoble
    Sep 22 at 20:16
  • 1
    @D.BenKnoble So I did. Thanks for doing the work of maintaining this place in a good shape. :)
    – cassepipe
    Sep 23 at 11:19
1

Here is the file in question with commented lines that contain useful tips to create your own ale linter. I hope it can provide a useful template for others to use :

" Description: norminette linter for C files.
"
" Get the norminette with :
"
" python3 -m pip install --upgrade pip setuptools
" python3 -m pip install norminette
"
" or at : https://github.com/42School/norminette

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.
    " :set incsearch to test your patterns in real-time !
    "
    "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 = ''
    
    "A good tip to check what is at each index of l:match is to run inside Vim :
    ":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)
    "                          ^^^^^^^^^^^^^^^^^^^^^^
    "                          Replace with each line you want to  match
    "               

    for l:match in ale#util#GetMatches(a:lines, l:pattern)
        if l:match[3] == 'OK'
            continue
        elseif l:match[3] == "Error"
            let l:curr_file = l:match[2]
        else
            call add(l:output, {
            \   'filename': l:curr_file,
            \   'lnum': str2nr(l:match[4]),
            \  'col': str2nr(l:match[5]),
            \   'type': 'W',
            \   'text': "Norminette : " . l:match[6],
            \})
        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',
\})

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.