2014-07-21 11:57:50 -04:00
#!/usr/bin/env lua
2014-07-17 16:19:52 -04:00
-- CheckBasicStyle.lua
--[[
Checks that all source files ( * . cpp , * . h ) use the basic style requirements of the project :
- Tabs for indentation , spaces for alignment
- Trailing whitespace on non - empty lines
- Two spaces between code and line - end comment ( " // " )
2014-07-19 09:08:49 -04:00
- Spaces after comma , not before
2014-07-19 09:25:28 -04:00
- Opening braces not at the end of a code line
2014-07-21 09:20:27 -04:00
- Spaces after if , for , while
2014-08-04 06:03:37 -04:00
- Line dividers ( //// ... ) exactly 80 slashes
2014-08-04 07:20:16 -04:00
- Multi - level indent change
2014-07-17 16:19:52 -04:00
- ( TODO ) Spaces before * , / , &
- ( TODO ) Hex numbers with even digit length
- ( TODO ) Hex numbers in lowercase
2014-07-19 09:08:49 -04:00
- ( TODO ) Not using " * " - style doxy comment continuation lines
2014-07-18 03:58:29 -04:00
Violations that cannot be checked easily :
- Spaces around " + " ( there are things like " a++ " , " ++a " , " a += 1 " , " X+ " , " stack +1 " and ascii - drawn tables )
2014-07-17 16:19:52 -04:00
Reports all violations on stdout in a form that is readable by Visual Studio ' s parser, so that dblclicking
the line brings the editor directly to the violation .
Returns 0 on success , 1 on internal failure , 2 if any violations found
--]]
-- The list of file extensions that are processed:
local g_ShouldProcessExt =
{
[ " h " ] = true ,
[ " cpp " ] = true ,
}
--- The list of files not to be processed:
local g_IgnoredFiles =
{
2014-10-13 04:33:08 -04:00
" Bindings/Bindings.h " ,
2014-07-21 11:38:36 -04:00
" Bindings/Bindings.cpp " ,
" LeakFinder.cpp " ,
" LeakFinder.h " ,
" MersenneTwister.h " ,
" StackWalker.cpp " ,
" StackWalker.h " ,
2014-07-17 16:19:52 -04:00
}
--- The list of files not to be processed, as a dictionary (filename => true), built from g_IgnoredFiles
local g_ShouldIgnoreFile = { }
-- Initialize the g_ShouldIgnoreFile map:
for _ , fnam in ipairs ( g_IgnoredFiles ) do
g_ShouldIgnoreFile [ fnam ] = true
end
--- Keeps track of the number of violations for this folder
local g_NumViolations = 0
--- Reports one violation
-- Pretty-prints the message
-- Also increments g_NumViolations
2014-07-19 09:08:49 -04:00
local function ReportViolation ( a_FileName , a_LineNumber , a_PatStart , a_PatEnd , a_Message )
print ( a_FileName .. " ( " .. a_LineNumber .. " ): " .. a_PatStart .. " .. " .. a_PatEnd .. " : " .. a_Message )
2014-07-17 16:19:52 -04:00
g_NumViolations = g_NumViolations + 1
end
2014-07-18 03:58:29 -04:00
--- Searches for the specified pattern, if found, reports it as a violation with the given message
local function ReportViolationIfFound ( a_Line , a_FileName , a_LineNum , a_Pattern , a_Message )
local patStart , patEnd = a_Line : find ( a_Pattern )
if not ( patStart ) then
return
end
2014-07-19 09:08:49 -04:00
ReportViolation ( a_FileName , a_LineNum , patStart , patEnd , a_Message )
2014-07-18 03:58:29 -04:00
end
local g_ViolationPatterns =
{
2014-12-05 06:58:30 -05:00
-- Parenthesis around comparisons:
{ " ==[^)]+&& " , " Add parenthesis around comparison " } ,
{ " &&[^(]+== " , " Add parenthesis around comparison " } ,
{ " ==[^)]+|| " , " Add parenthesis around comparison " } ,
{ " ||[^(]+== " , " Add parenthesis around comparison " } ,
{ " !=[^)]+&& " , " Add parenthesis around comparison " } ,
{ " &&[^(]+!= " , " Add parenthesis around comparison " } ,
{ " !=[^)]+|| " , " Add parenthesis around comparison " } ,
{ " ||[^(]+!= " , " Add parenthesis around comparison " } ,
2015-03-21 15:35:25 -04:00
{ " <[^)>]*&& " , " Add parenthesis around comparison " } , -- Must take special care of templates: "template <T> fn(Args && ...)"
2015-03-11 06:39:49 -04:00
-- Cannot check a < following a && due to functions of the form x fn(y&& a, z<b> c)
2015-03-21 15:35:25 -04:00
{ " <[^)>]*|| " , " Add parenthesis around comparison " } , -- Must take special care of templates: "template <T> fn(Args && ...)"
2014-12-05 06:58:30 -05:00
{ " ||[^(]+< " , " Add parenthesis around comparison " } ,
-- Cannot check ">" because of "obj->m_Flag &&". Check at least ">=":
{ " >=[^)]+&& " , " Add parenthesis around comparison " } ,
{ " &&[^(]+>= " , " Add parenthesis around comparison " } ,
{ " >=[^)]+|| " , " Add parenthesis around comparison " } ,
{ " ||[^(]+>= " , " Add parenthesis around comparison " } ,
2014-07-18 03:58:29 -04:00
-- Check against indenting using spaces:
{ " ^ \t * + " , " Indenting with a space " } ,
-- Check against alignment using tabs:
{ " [^%s] \t +[^%s] " , " Aligning with a tab " } ,
-- Check against trailing whitespace:
{ " [^%s]%s+ \n " , " Trailing whitespace " } ,
-- Check that all "//"-style comments have at least two spaces in front (unless alone on line):
{ " [^%s] // " , " Needs at least two spaces in front of a \" // \" -style comment " } ,
-- Check that all "//"-style comments have at least one spaces after:
{ " %s//[^%s/*<] " , " Needs a space after a \" // \" -style comment " } ,
-- Check that all commas have spaces after them and not in front of them:
{ " , " , " Extra space before a \" , \" " } ,
2014-08-28 09:53:26 -04:00
{ " ,[^%s \" %% \' ] " , " Needs a space after a \" , \" " } , -- Report all except >> "," << needed for splitting and >>,%s<< needed for formatting
2014-07-19 09:25:28 -04:00
-- Check that opening braces are not at the end of a code line:
{ " [^%s].-{ \n ?$ " , " Brace should be on a separate line " } ,
2014-07-21 09:20:27 -04:00
-- Space after keywords:
{ " [^_]if%( " , " Needs a space after \" if \" " } ,
2014-12-04 16:44:24 -05:00
{ " %sfor%( " , " Needs a space after \" for \" " } ,
{ " %swhile%( " , " Needs a space after \" while \" " } ,
{ " %sswitch%( " , " Needs a space after \" switch \" " } ,
{ " %scatch%( " , " Needs a space after \" catch \" " } ,
{ " %stemplate< " , " Needs a space after \" template \" " } ,
2014-07-21 09:20:27 -04:00
-- No space after keyword's parenthesis:
{ " [^%a#]if %( " , " Remove the space after \" ( \" " } ,
{ " for %( " , " Remove the space after \" ( \" " } ,
{ " while %( " , " Remove the space after \" ( \" " } ,
2014-07-21 09:21:54 -04:00
{ " catch %( " , " Remove the space after \" ( \" " } ,
2014-07-21 09:20:27 -04:00
-- No space before a closing parenthesis:
{ " %) " , " Remove the space before \" ) \" " } ,
2014-07-18 03:58:29 -04:00
}
2014-07-17 16:19:52 -04:00
--- Processes one file
local function ProcessFile ( a_FileName )
assert ( type ( a_FileName ) == " string " )
-- Read the whole file:
local f , err = io.open ( a_FileName , " r " )
if ( f == nil ) then
print ( " Cannot open file \" " .. a_FileName .. " \" : " .. err )
print ( " Aborting " )
os.exit ( 1 )
end
local all = f : read ( " *all " )
2015-01-29 05:10:32 -05:00
f : close ( )
2014-07-17 16:19:52 -04:00
-- Check that the last line is empty - otherwise processing won't work properly:
local lastChar = string.byte ( all , string.len ( all ) )
if ( ( lastChar ~= 13 ) and ( lastChar ~= 10 ) ) then
local numLines = 1
string.gsub ( all , " \n " , function ( ) numLines = numLines + 1 end ) -- Count the number of line-ends
2014-07-19 09:25:28 -04:00
ReportViolation ( a_FileName , numLines , 1 , 1 , " Missing empty line at file end " )
2014-07-17 16:19:52 -04:00
return
end
-- Process each line separately:
-- Ref.: http://stackoverflow.com/questions/10416869/iterate-over-possibly-empty-lines-in-a-way-that-matches-the-expectations-of-exis
local lineCounter = 1
2014-08-04 07:20:16 -04:00
local lastIndentLevel = 0
2014-12-05 10:59:56 -05:00
local isLastLineControl = false
2014-07-17 16:19:52 -04:00
all : gsub ( " \r \n " , " \n " ) -- normalize CRLF into LF-only
string.gsub ( all .. " \n " , " [^ \n ]* \n " , -- Iterate over each line, while preserving empty lines
function ( a_Line )
2014-07-18 03:58:29 -04:00
-- Check against each violation pattern:
for _ , pat in ipairs ( g_ViolationPatterns ) do
ReportViolationIfFound ( a_Line , a_FileName , lineCounter , pat [ 1 ] , pat [ 2 ] )
2014-07-17 16:19:52 -04:00
end
2014-08-04 06:03:37 -04:00
2014-08-04 07:20:16 -04:00
-- Check that divider comments are well formed - 80 slashes plus optional indent:
2014-08-04 06:03:37 -04:00
local dividerStart , dividerEnd = a_Line : find ( " /////* " )
if ( dividerStart ) then
if ( dividerEnd ~= dividerStart + 79 ) then
ReportViolation ( a_FileName , lineCounter , 1 , 80 , " Divider comment should have exactly 80 slashes " )
end
if ( dividerStart > 1 ) then
if (
( a_Line : sub ( 1 , dividerStart - 1 ) ~= string.rep ( " \t " , dividerStart - 1 ) ) or -- The divider should have only indent in front of it
( a_Line : len ( ) > dividerEnd + 1 ) -- The divider should have no other text following it
) then
ReportViolation ( a_FileName , lineCounter , 1 , 80 , " Divider comment shouldn't have any extra text around it " )
end
end
end
2014-08-04 07:20:16 -04:00
-- Check the indent level change from the last line, if it's too much, report:
local indentStart , indentEnd = a_Line : find ( " \t + " )
local indentLevel = 0
if ( indentStart ) then
indentLevel = indentEnd - indentStart
end
if ( indentLevel > 0 ) then
if ( ( lastIndentLevel - indentLevel >= 2 ) or ( lastIndentLevel - indentLevel <= - 2 ) ) then
ReportViolation ( a_FileName , lineCounter , 1 , indentStart or 1 , " Indent changed more than a single level between the previous line and this one: from " .. lastIndentLevel .. " to " .. indentLevel )
end
lastIndentLevel = indentLevel
end
2014-12-05 10:59:56 -05:00
-- Check that control statements have braces on separate lines after them:
-- Note that if statements can be broken into multiple lines, in which case this test is not taken
if ( isLastLineControl ) then
if not ( a_Line : find ( " ^%s*{ " ) or a_Line : find ( " ^%s*# " ) ) then
-- Not followed by a brace, not followed by a preprocessor
ReportViolation ( a_FileName , lineCounter - 1 , 1 , 1 , " Control statement needs a brace on separate line " )
end
end
local lineWithSpace = " " .. a_Line
isLastLineControl =
lineWithSpace : find ( " ^%s+if %b() " ) or
lineWithSpace : find ( " ^%s+else if %b() " ) or
lineWithSpace : find ( " ^%s+for %b() " ) or
lineWithSpace : find ( " ^%s+switch %b() " ) or
lineWithSpace : find ( " ^%s+else \n " ) or
lineWithSpace : find ( " ^%s+else // " ) or
lineWithSpace : find ( " ^%s+do %b() " )
2014-07-18 03:58:29 -04:00
2014-07-17 16:19:52 -04:00
lineCounter = lineCounter + 1
end
)
end
--- Processes one item - a file or a folder
local function ProcessItem ( a_ItemName )
assert ( type ( a_ItemName ) == " string " )
-- Skip files / folders that should be ignored
if ( g_ShouldIgnoreFile [ a_ItemName ] ) then
return
end
local ext = a_ItemName : match ( " %.([^/%.]-)$ " )
if ( g_ShouldProcessExt [ ext ] ) then
ProcessFile ( a_ItemName )
end
end
2015-05-02 07:02:18 -04:00
--- Array of files to process. Filled from cmdline arguments
local ToProcess = { }
--- Handlers for the command-line arguments
-- Maps flag => function
local CmdLineHandlers =
{
-- "-f file" checks the specified file
[ " -f " ] = function ( a_Args , a_Idx )
local fnam = a_Args [ a_Idx + 1 ]
if not ( fnam ) then
error ( " Invalid flag: '-f' needs a filename following it. " )
end
table.insert ( ToProcess , fnam )
return a_Idx + 2 -- skip the filename in param parsing
end ,
-- "-g" checks files reported by git as being committed.
[ " -g " ] = function ( a_Args , a_Idx )
local f = io.popen ( " git diff --cached --name-only --diff-filter=ACMR " )
for fnam in f : lines ( ) do
table.insert ( ToProcess , fnam )
end
end ,
-- "-h" prints help and exits
[ " -h " ] = function ( a_Args , a_Idx )
print ( [ [
Usage : " )
" CheckBasicStyle [<options>]
Available options :
- f < filename > - checks the specified filename
- g - checks files reported by Git as being committed
- h - prints this help and exits
- l < listfile > - checks all files listed in the specified listfile
-- - reads the list of files to check from stdin
When no options are given , the script checks all files listed in the AllFiles.lst file .
Only . cpp and . h files are ever checked .
] ] )
os.exit ( 0 )
end ,
-- "-l listfile" loads the list of files to check from the specified listfile
[ " -l " ] = function ( a_Args , a_Idx )
local listFile = a_Args [ a_Idx + 1 ]
if not ( listFile ) then
error ( " Invalid flag: '-l' needs a filename following it. " )
end
for fnam in io.lines ( listFile ) do
table.insert ( ToProcess , fnam )
end
return a_Idx + 2 -- Skip the listfile in param parsing
end ,
-- "--" reads the list of files from stdin
[ " -- " ] = function ( a_Args , a_Idx )
for fnam in io.lines ( ) do
table.insert ( ToProcess , fnam )
end
end ,
}
2014-12-05 06:58:30 -05:00
-- Remove buffering from stdout, so that the output appears immediately in IDEs:
io.stdout : setvbuf ( " no " )
2015-05-02 07:02:18 -04:00
-- Parse the cmdline arguments to see what files to check:
local idx = 1
while ( arg [ idx ] ) do
local handler = CmdLineHandlers [ arg [ idx ] ]
if not ( handler ) then
error ( " Unknown command-line argument # " .. idx .. " : " .. arg [ idx ] )
end
idx = handler ( arg , idx ) or ( idx + 1 ) -- Call the handler, let it change the next index if it wants
end
-- By default process all files in the AllFiles.lst file (generated by cmake):
if not ( arg [ 1 ] ) then
for fnam in io.lines ( " AllFiles.lst " ) do
table.insert ( ToProcess , fnam )
end
end
-- Process the files in the list:
for _ , fnam in ipairs ( ToProcess ) do
2014-07-21 11:38:36 -04:00
ProcessItem ( fnam )
end
2014-07-17 16:19:52 -04:00
2015-05-02 07:02:18 -04:00
2014-07-17 16:19:52 -04:00
-- Report final verdict:
print ( " Number of violations found: " .. g_NumViolations )
if ( g_NumViolations > 0 ) then
os.exit ( 2 )
else
os.exit ( 0 )
end