#!/usr/bin/env lua -- 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 ("//") - Spaces after comma, not before - Opening braces not at the end of a code line - Spaces after if, for, while - Line dividers (////...) exactly 80 slashes - Multi-level indent change - (TODO) Spaces before *, /, & - (TODO) Hex numbers with even digit length - (TODO) Hex numbers in lowercase - (TODO) Not using "* "-style doxy comment continuation lines Violations that cannot be checked easily: - Spaces around "+" (there are things like "a++", "++a", "a += 1", "X+", "stack +1" and ascii-drawn tables) 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 = { "Bindings/Bindings.h", "Bindings/Bindings.cpp", "LeakFinder.cpp", "LeakFinder.h", "MersenneTwister.h", "StackWalker.cpp", "StackWalker.h", } --- 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 local function ReportViolation(a_FileName, a_LineNumber, a_PatStart, a_PatEnd, a_Message) print(a_FileName .. "(" .. a_LineNumber .. "): " .. a_PatStart .. " .. " .. a_PatEnd .. ": " .. a_Message) g_NumViolations = g_NumViolations + 1 end --- 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 ReportViolation(a_FileName, a_LineNum, patStart, patEnd, a_Message) end local g_ViolationPatterns = { -- 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"}, {"<[^)T][^)]*&&", "Add parenthesis around comparison"}, -- Must take special care of templates: "template fn(Args && ...)" {"&&[^(]+<", "Add parenthesis around comparison"}, {"<[^)T][^)]*||", "Add parenthesis around comparison"}, -- Must take special care of templates: "template fn(Args && ...)" {"||[^(]+<", "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"}, -- 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 \",\""}, {",[^%s\"%%\']", "Needs a space after a \",\""}, -- Report all except >> "," << needed for splitting and >>,%s<< needed for formatting -- Check that opening braces are not at the end of a code line: {"[^%s].-{\n?$", "Brace should be on a separate line"}, -- Space after keywords: {"[^_]if%(", "Needs a space after \"if\""}, {"%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\""}, -- No space after keyword's parenthesis: {"[^%a#]if %( ", "Remove the space after \"(\""}, {"for %( ", "Remove the space after \"(\""}, {"while %( ", "Remove the space after \"(\""}, {"catch %( ", "Remove the space after \"(\""}, -- No space before a closing parenthesis: {" %)", "Remove the space before \")\""}, } --- 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") -- 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 ReportViolation(a_FileName, numLines, 1, 1, "Missing empty line at file end") 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 local lastIndentLevel = 0 local isLastLineControl = false 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) -- Check against each violation pattern: for _, pat in ipairs(g_ViolationPatterns) do ReportViolationIfFound(a_Line, a_FileName, lineCounter, pat[1], pat[2]) end -- Check that divider comments are well formed - 80 slashes plus optional indent: 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 -- 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 -- 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()") 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 -- Remove buffering from stdout, so that the output appears immediately in IDEs: io.stdout:setvbuf("no") -- Process all files in the AllFiles.lst file (generated by cmake): for fnam in io.lines("AllFiles.lst") do ProcessItem(fnam) end -- Report final verdict: print("Number of violations found: " .. g_NumViolations) if (g_NumViolations > 0) then os.exit(2) else os.exit(0) end