NazaraEngine/xmake/actions/checkfiles.lua

877 lines
23 KiB
Lua

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
return false
end
end
return true
end
local function ComputeHeaderFile(filePath)
local headerPath = filePath:gsub("[\\/]", "/")
headerPath = headerPath:sub(headerPath:find("Nazara/"), -1)
headerPath = headerPath:sub(1, headerPath:find("%..+$") - 1) .. ".hpp"
return headerPath
end
local systemHeaders = {
["fcntl.h"] = true,
["mstcpip"] = true,
["netdb.h"] = true,
["poll.h"] = true,
["process.h"] = true,
["pthread.h"] = true,
["signal.h"] = true,
["spawn.h"] = true,
["unistd.h"] = true,
["windows.h"] = true,
["winsock2"] = true,
}
local function IsSystemHeader(header)
if systemHeaders[header:lower()] then
return true
end
if header:match("netinet/.*") then
return true
end
if header:match("sys/.*") then
return true
end
return false
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 = {}
-- Remove empty lines at the beginning of files
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
})
-- Check header guards and #pragma once
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
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
end
return fixes
end
})
-- Every source file should include its header first, except .inl files
table.insert(checks, {
Name = "inclusion",
Check = function (moduleName)
local files = table.join(
os.files("include/Nazara/" .. moduleName .. "/**.inl"),
os.files("src/Nazara/" .. moduleName .. "/**.inl"),
os.files("src/Nazara/" .. moduleName .. "/**.cpp")
)
local fixes = {}
for _, filePath in pairs(files) do
local lines = GetFile(filePath)
local headerPath = ComputeHeaderFile(filePath)
if os.isfile("include/" .. headerPath) or os.isfile("src/" .. headerPath) then
local inclusions = {}
-- Retrieve all inclusions
for i = 1, #lines do
if lines[i] == "// no include fix" then
-- ignore file
inclusions = {}
break
end
local includeMode, includePath = lines[i]:match("^#include%s*([<\"])(.+)[>\"]")
if includeMode then
table.insert(inclusions, {
line = i,
mode = includeMode,
path = includePath
})
end
end
if #inclusions > 0 then
local increment = 0
-- Add corresponding header
local headerInclude
for i = 1, #inclusions do
if inclusions[i].path == headerPath then
headerInclude = i
break
end
end
-- Check debug headers
local debugIncludeModule = moduleName ~= "Math" and moduleName or "Core"
local debugInclude
local debugIncludeOff
for i = 1, #inclusions do
local module, off = inclusions[i].path:match("^Nazara/(.+)/Debug(O?f?f?).hpp$")
if module then
if off == "Off" then
debugIncludeOff = i
elseif off == "" then
debugInclude = i
else
print(filePath .. ": unrecognized debug include at line " .. inclusions[i].line)
end
if module ~= debugIncludeModule then
print(filePath .. ": has wrong Debug" .. off .. " include")
local currentInclusion = inclusions[i]
table.insert(fixes, {
File = filePath,
Func = function (lines)
lines[currentInclusion.line] = "#include <Nazara/" .. debugIncludeModule .. "/Debug" .. off .. ".hpp>"
return lines
end
})
end
end
end
-- Add header inclusion if it's missing
local isInl = path.extension(filePath) == ".inl"
if not headerInclude and not isInl then
print(filePath .. " is missing corresponding header inclusion")
table.insert(fixes, {
File = filePath,
Func = function (lines)
local firstHeaderLine = inclusions[1].line
table.insert(lines, firstHeaderLine, "#include <" .. headerPath .. ">")
increment = increment + 1
return lines
end
})
elseif headerInclude and isInl then
print(filePath .. " has a header inclusion which breaks clangd (.inl should no longer includes their .hpp)")
table.insert(fixes, {
File = filePath,
Func = function (lines)
table.remove(lines, inclusions[headerInclude].line)
increment = increment - 1
return lines
end
})
end
if not debugInclude then
print(filePath .. ": has missing Debug include")
local lastIncludeLine = inclusions[debugIncludeOff and #inclusions - 1 or #inclusions].line
table.insert(fixes, {
File = filePath,
Func = function (lines)
table.insert(lines, lastIncludeLine + increment + 1, "#include <Nazara/" .. debugIncludeModule .. "/Debug.hpp>")
return lines
end
})
end
if isInl then
if not debugIncludeOff then
print(filePath .. ": has missing DebugOff include")
table.insert(fixes, {
File = filePath,
Func = function (lines)
table.insert(lines, "")
table.insert(lines, "#include <Nazara/" .. debugIncludeModule .. "/DebugOff.hpp>")
table.insert(lines, "")
return lines
end
})
end
end
end
end
end
return fixes
end
})
-- Reorder includes and remove duplicates
table.insert(checks, {
Name = "inclusion order",
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)
local headerPath = ComputeHeaderFile(filePath)
local inclusions = {}
-- Retrieve all inclusions from the first inclusion block
for i = 1, #lines do
if lines[i] == "// no include reordering" then
-- ignore file
inclusions = {}
break
end
local includeMode, includePath = lines[i]:match("^#include%s*([<\"])(.+)[>\"]")
if includeMode then
table.insert(inclusions, {
line = i,
mode = includeMode,
path = includePath
})
elseif #inclusions > 0 then
-- Stop when outside the inclusion block
break
end
end
local debugIncludeModule = moduleName ~= "Math" and moduleName or "Core"
local includeList = {}
local shouldReorder = false
for i = 1, #inclusions do
local order
if inclusions[i].path == headerPath then
order = 0 -- own include comes first
elseif inclusions[i].path == "NazaraUtils/Prerequisites.hpp" then
order = 1 -- top engine includes
elseif inclusions[i].path == "Nazara/" .. debugIncludeModule .. "/Debug.hpp" then
order = 6 -- debug include
elseif inclusions[i].path:match("^NazaraUtils/") then
order = 2.1 -- NazaraUtils
elseif inclusions[i].path:match("^Nazara/") then
order = 2 -- engine includes
elseif IsSystemHeader(inclusions[i].path) then
order = 5 -- system includes
elseif inclusions[i].path:match(".+%.hp?p?") then
order = 3 -- thirdparty includes
else
order = 4 -- standard includes
end
table.insert(includeList, {
order = order,
path = inclusions[i].path,
content = lines[inclusions[i].line]
})
end
local function compareFunc(a, b)
if a.order == b.order then
local folderA = a.path:match("^(.-)/")
local folderB = b.path:match("^(.-)/")
if folderA and folderB then
if folderA ~= folderB then
return folderA < folderB
end
local moduleA = a.path:match("^Nazara/(.-)/")
local moduleB = b.path:match("^Nazara/(.-)/")
if moduleA ~= moduleB then
return moduleA < moduleB
end
end
local _, folderCountA = a.path:gsub("/", "")
local _, folderCountB = b.path:gsub("/", "")
if folderCountA == folderCountB then
return a.path < b.path
else
return folderCountA < folderCountB
end
else
return a.order < b.order
end
end
local isOrdered = true
for i = 2, #includeList do
if includeList[i - 1].path == includeList[i].path then
-- duplicate found
print(filePath .. ": include list has duplicates")
isOrdered = false
break
end
if not compareFunc(includeList[i - 1], includeList[i]) then
print(filePath .. ": include list is not ordered")
isOrdered = false
break
end
end
if not isOrdered then
table.sort(includeList, compareFunc)
table.insert(fixes, {
File = filePath,
Func = function (lines)
-- Reorder includes
local newInclusions = {}
for i = 1, #inclusions do
lines[inclusions[i].line] = includeList[i].content
table.insert(newInclusions, {
content = includeList[i].content,
path = includeList[i].path,
line = inclusions[i].line
})
end
-- Remove duplicates
table.sort(newInclusions, function (a, b) return a.line > b.line end)
for i = 2, #newInclusions do
local a = newInclusions[i - 1]
local b = newInclusions[i]
if a.path == b.path then
if #a.content > #b.content then -- keep longest line (for comments)
table.remove(lines, b.line)
else
table.remove(lines, a.line)
end
end
end
return lines
end
})
end
end
return fixes
end
})
-- Check copyright date and format
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 not authors or 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)