NazaraEngine/xmake/actions/checkfiles.lua

537 lines
14 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
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)