task("check-files") set_menu({ -- Settings menu usage usage = "xmake check-files [options]", description = "Check every file for consistency (can fix some errors)", options = { -- Set k mode as key-only bool parameter {'f', "fix", "k", nil, "Attempt to automatically fix files." } } }) local function CompareLines(referenceLines, lines, firstLine, lineCount) firstLine = firstLine or 1 lineCount = lineCount or (#lines - firstLine + 1) if lineCount ~= #referenceLines then return false end for i = 1, lineCount do if lines[firstLine + i - 1] ~= referenceLines[i] then print(lines[firstLine + i]) print(referenceLines[i]) return false end end return true end on_run(function () import("core.base.option") local modules = os.dirs("include/Nazara/*") local fileLines = {} local updatedFiles = {} local function GetFile(filePath) filePath = path.translate(filePath) local lines = fileLines[filePath] if not lines then lines = table.to_array(io.lines(filePath)) if not lines then os.raise("failed to open " .. filePath) end fileLines[filePath] = lines end return lines end local function UpdateFile(filePath, lines) filePath = path.translate(filePath) if lines then fileLines[filePath] = lines end updatedFiles[filePath] = true end local checks = {} table.insert(checks, { Name = "empty lines", Check = function (moduleName) local files = table.join( os.files("include/Nazara/" .. moduleName .. "/**.hpp"), os.files("include/Nazara/" .. moduleName .. "/**.inl"), os.files("src/Nazara/" .. moduleName .. "/**.hpp"), os.files("src/Nazara/" .. moduleName .. "/**.inl"), os.files("src/Nazara/" .. moduleName .. "/**.cpp") ) local fixes = {} for _, filePath in pairs(files) do local lines = GetFile(filePath) for i = 1, #lines do if not lines[i]:match("^%s*$") then if i ~= 1 then print(filePath .. " starts with empty lines") table.insert(fixes, { File = filePath, Func = function (lines) for j = 1, i - 1 do table.remove(lines, 1) end UpdateFile(filePath, lines) end }) end break end end end return fixes end }) table.insert(checks, { Name = "header guards", Check = function (moduleName) local files = table.join( os.files("include/Nazara/" .. moduleName .. "/**.hpp"), os.files("src/Nazara/" .. moduleName .. "/**.hpp") ) local fixes = {} for _, filePath in pairs(files) do local lines = GetFile(filePath) local pragmaLine local ifndefLine local defineLine local endifLine local macroName local pathMacro = filePath:gsub("[/\\]", "_") do pathMacro = pathMacro:sub(pathMacro:lastof(moduleName .. "_", true) + #moduleName + 1) local i = pathMacro:lastof(".", true) if i then pathMacro = pathMacro:sub(1, i - 1) end end local pathHeaderGuard = (moduleName ~= pathMacro) and "NAZARA_" .. moduleName:upper() .. "_" .. pathMacro:upper() .. "_HPP" or "NAZARA_" .. moduleName:upper() .. "_HPP" local canFix = true local ignored = false -- Fetch pragma once, ifdef and define lines for i = 1, #lines do if lines[i] == "// no header guards" then canFix = false ignored = true break end if lines[i] == "#pragma once" then if pragmaLine then print(filePath .. ": multiple #pragma once found") canFix = false break end pragmaLine = i elseif not ifndefLine and lines[i]:startswith("#ifndef") then ifndefLine = i macroName = lines[i]:match("^#ifndef%s+(.+)$") if not macroName then print(filePath .. ": failed to identify header guard macro (ifndef)") canFix = false break end elseif ifndefLine and not defineLine and lines[i]:startswith("#define") then defineLine = i local defineMacroName = lines[i]:match("^#define%s+(.+)$") if not defineMacroName then print(filePath .. ": failed to identify header guard macro (define)") canFix = false break end if defineMacroName ~= macroName then print(filePath .. ": failed to identify header guard macro (define macro doesn't match ifdef)") canFix = false break end end if ifndefLine and defineLine then break end end if not ignored then if not ifndefLine or not defineLine or not macroName then print(filePath .. ": failed to identify header guard macro") canFix = false end -- Fetch endif line if canFix then local shouldFixEndif = false for i = #lines, 1, -1 do if lines[i]:startswith("#endif") then local macro = lines[i]:match("#endif // (.+)") if macro ~= macroName then shouldFixEndif = true end endifLine = i break end end if not endifLine then print(filePath .. ": failed to identify header guard macro (endif)") canFix = false end end if canFix then if macroName ~= pathHeaderGuard then print(filePath .. ": header guard mismatch (got " .. macroName .. ", expected " .. pathHeaderGuard .. ")") shouldFixEndif = false table.insert(fixes, { File = filePath, Func = function (lines) lines[ifndefLine] = "#ifndef " .. pathHeaderGuard lines[defineLine] = "#define " .. pathHeaderGuard lines[endifLine] = "#endif // " .. pathHeaderGuard return lines end }) end if shouldFixEndif then print(filePath .. ": #endif was missing comment") table.insert(fixes, { File = filePath, Func = function (lines) lines[endifLine] = "#endif // " .. pathHeaderGuard return lines end }) end if not pragmaLine then print(filePath .. ": no #pragma once found") table.insert(fixes, { File = filePath, Func = function (lines) table.insert(lines, ifndefLine - 1, "#pragma once") table.insert(lines, ifndefLine - 1, "") return lines end }) elseif pragmaLine > ifndefLine then print(filePath .. ": #pragma once is after header guard (should be before)") end end end end return fixes end }) table.insert(checks, { Name = "copyright", Check = function (moduleName) local files = table.join( os.files("include/Nazara/" .. moduleName .. "/**.hpp|Config.hpp"), os.files("include/Nazara/" .. moduleName .. "/**.inl"), os.files("src/Nazara/" .. moduleName .. "/**.hpp"), os.files("src/Nazara/" .. moduleName .. "/**.inl"), os.files("src/Nazara/" .. moduleName .. "/**.cpp") ) local currentYear = os.date("%Y") local engineAuthor = "Jérôme \"Lynix\" Leclercq (lynix680@gmail.com)" local prevAuthor = "Jérôme Leclercq" local fixes = {} local moduleDesc if #moduleName > 8 and moduleName:endswith("Renderer") then moduleDesc = moduleName:sub(1, -9) .. " renderer" else moduleDesc = moduleName .. " module" end -- Config do local configFilePath = path.translate("include/Nazara/" .. moduleName .. "/Config.hpp") local lines = GetFile(configFilePath) local pragmaLine for i = 1, #lines do if lines[i] == "#pragma once" then pragmaLine = i break end end local canFix = true repeat if not pragmaLine then print(configFilePath .. ": pragma once not found") canFix = false break end local licenseText = [[ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #pragma once]] local licenseLines = licenseText:split("\r?\n", { strict = true }) local shouldFix = false -- Try to retrieve year and authors if lines[1] ~= "/*" then print(configFilePath .. ": file doesn't begin with block comment") break end if lines[2]:match("\tNazara Engine - " .. moduleDesc) then print(configFilePath .. ": module description doesn't match") shouldFix = true end if lines[3] ~= "" then -- Not really empty because of space characters? Not a big deal if lines[3]:match("^%s*$") then shouldFix = true else print(configFilePath .. ": expected space after project name") break end end local year, moduleAuthor = lines[4]:match("^\tCopyright %(C%) (Y?E?A?R?%d*) (.+)$") if not year then print(configFilePath .. ": couldn't parse copyright date and author") break end if year ~= currentYear then print(configFilePath .. ": incorrect copyright year") shouldFix = true end local additionalAuthors = {} for i = 5, #lines do if lines[i]:match("^%s*$") then -- Empty line if not CompareLines(licenseLines, lines, i, pragmaLine - i + 1) then shouldFix = true end break end if lines[i]:match("%s*Permission is hereby granted") then print(configFilePath .. ": missing empty line before licence text") shouldFix = true break end table.insert(additionalAuthors, lines[i]:match("%s*(.+)")) end if shouldFix then table.insert(fixes, { File = configFilePath, Func = function (lines) local newLines = { "/*", "\tNazara Engine - " .. moduleDesc, "", } local copyrightNotice = "Copyright (C) " .. currentYear table.insert(newLines, "\t" .. copyrightNotice .. " " .. moduleAuthor) for _, author in ipairs(additionalAuthors) do table.insert(newLines, "\t" .. string.rep(" ", #copyrightNotice) .. " " .. author) end for _, line in ipairs(licenseLines) do table.insert(newLines, line) end for i = pragmaLine + 1, #lines do table.insert(newLines, lines[i]) end return newLines end }) end until true if not canFix then print(configFilePath .. ": header couldn't be parsed, no automated fix will be done") end end -- Headers for _, filePath in pairs(files) do local lines = GetFile(filePath) local hasCopyright local shouldFix = false if lines[1]:lower():match("^// this file was automatically generated") then goto skip end local year, authors = lines[1]:match("^// Copyright %(C%) (Y?E?A?R?%d*) (.+)$") hasCopyright = year ~= nil if authors == "AUTHORS" then authors = engineAuthor else local fixedAuthors = authors:gsub(prevAuthor, engineAuthor) if fixedAuthors ~= authors then authors = fixedAuthors shouldFix = true end end if hasCopyright then if year ~= currentYear then print(filePath .. ": copyright year error") shouldFix = true end local copyrightModule = lines[2]:match("^// This file is part of the \"Nazara Engine %- (.+)\"$") if copyrightModule ~= moduleDesc then print(filePath .. ": copyright module error") shouldFix = true end if lines[3] ~= "// For conditions of distribution and use, see copyright notice in Config.hpp" then print(filePath .. ": copyright file reference error") shouldFix = true end else print(filePath .. ": copyright not found") shouldFix = true end if shouldFix then table.insert(fixes, { File = filePath, Func = function(lines) local copyrightLines = { "// Copyright (C) " .. currentYear .. " " .. (authors or engineAuthor), "// This file is part of the \"Nazara Engine - " .. moduleDesc .. "\"", "// For conditions of distribution and use, see copyright notice in Config.hpp" } if hasCopyright then for i, line in ipairs(copyrightLines) do lines[i] = line end else for i, line in ipairs(copyrightLines) do table.insert(lines, i, line) end table.insert(lines, #copyrightLines + 1, "") end return lines end }) end ::skip:: end return fixes end }) local shouldFix = option.get("fix") or false for _, check in pairs(checks) do print("Running " .. check.Name .. " check...") local fixes = {} for _, modulePath in pairs(modules) do local moduleName = modulePath:match(".*[\\/](.*)") table.join2(fixes, check.Check(moduleName)) end if shouldFix then for _, fix in pairs(fixes) do print("Fixing " .. fix.File) UpdateFile(fix.File, fix.Func(assert(fileLines[fix.File]))) end end end for filePath, _ in pairs(updatedFiles) do local lines = assert(fileLines[filePath]) if lines[#lines] ~= "" then table.insert(lines, "") end print("Saving changes to " .. filePath) io.writefile(filePath, table.concat(lines, "\n")) end end)