main.lua 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. local serpent = require 'serpent'
  2. local f = io.popen('git branch --show-current')
  3. local dev = f and f:read('*a'):match('%w+') == 'dev'
  4. f:close()
  5. -- Helpers
  6. local function copy(t)
  7. if type(t) ~= 'table' then return t end
  8. local result = {}
  9. for k, v in pairs(t) do
  10. result[k] = copy(v)
  11. end
  12. return result
  13. end
  14. local function unindent(code)
  15. local indent = code:match('^(% +)')
  16. if indent then
  17. return code:gsub('\n' .. indent, '\n'):gsub('^' .. indent, ''):gsub('%s*$', '')
  18. else
  19. return code
  20. end
  21. end
  22. local function unwrap(str)
  23. if not str then return str end
  24. str = unindent(str)
  25. return str:gsub('([^\n])\n(%S)', function(a, b)
  26. if b == '-' or b == '>' then
  27. return a .. '\n' .. b
  28. else
  29. return a .. ' ' .. b
  30. end
  31. end)
  32. :gsub('^%s+', '')
  33. :gsub('%s+$', '')
  34. end
  35. local function pluralify(t, key)
  36. t[key .. 's'] = t[key .. 's'] or (t[key] and { t[key] } or nil)
  37. t[key] = nil
  38. return t[key .. 's']
  39. end
  40. local lookup = {}
  41. local function track(obj)
  42. lookup[obj.key] = obj
  43. end
  44. local function warn(s, ...)
  45. print(string.format(s, ...))
  46. end
  47. local function warnIf(cond, ...)
  48. if cond then warn(...) end
  49. end
  50. -- Processors
  51. local function processExample(example, key)
  52. if type(example) == 'string' then
  53. return {
  54. code = unindent(example)
  55. }
  56. else
  57. assert(example.code, string.format('%s example is missing code', key))
  58. example.description = unwrap(example.description)
  59. example.code = unindent(example.code)
  60. end
  61. return example
  62. end
  63. local function processEnum(path, parent)
  64. local enum = require(path)
  65. enum.name = path:match('[^/]+$')
  66. enum.key = enum.name
  67. enum.module = parent.key
  68. enum.description = unwrap(enum.description)
  69. enum.notes = unwrap(enum.notes)
  70. for _, value in ipairs(enum.values) do
  71. value.description = unwrap(value.description)
  72. end
  73. track(enum)
  74. return enum
  75. end
  76. local function processFunction(path, parent)
  77. local fn = require(path)
  78. fn.name = path:match('[^/]+$')
  79. fn.key = parent.name:match('^[A-Z]') and (parent.key .. ':' .. fn.name) or (path:gsub('/', '.'):gsub('callbacks%.', ''))
  80. fn.description = unwrap(fn.description)
  81. fn.module = parent.module or parent.key
  82. fn.notes = unwrap(fn.notes)
  83. fn.examples = pluralify(fn, 'example')
  84. for k, example in ipairs(fn.examples or {}) do
  85. fn.examples[k] = processExample(example, fn.key)
  86. end
  87. assert(fn.variants, string.format('Function %q is missing variants', fn.key))
  88. assert(fn.arguments, string.format('Function %q does not have arguments list', fn.key))
  89. assert(fn.returns, string.format('Function %q does not have returns list', fn.key))
  90. for name, arg in pairs(fn.arguments) do
  91. arg.name = name
  92. end
  93. for name, ret in pairs(fn.returns) do
  94. ret.name = name
  95. end
  96. for i, variant in ipairs(fn.variants) do
  97. assert(variant.arguments, string.format('%q variant #%d is missing arguments', fn.key, i))
  98. assert(variant.returns, string.format('%q variant #%d is missing returns', fn.key, i))
  99. for j, name in ipairs(variant.arguments) do
  100. warnIf(not fn.arguments[name], string.format('%s uses unknown argument %s', fn.key, name))
  101. variant.arguments[j] = copy(fn.arguments[name])
  102. end
  103. for j, name in ipairs(variant.returns) do
  104. warnIf(not fn.returns[name], string.format('%s uses unknown return %s', fn.key, name))
  105. variant.returns[j] = copy(fn.returns[name])
  106. end
  107. end
  108. for _, variant in ipairs(fn.variants) do
  109. local function processTable(t)
  110. if not t then return end
  111. for _, field in ipairs(t) do
  112. field.description = unwrap(field.description)
  113. processTable(field.table)
  114. end
  115. t.description = unwrap(t.description)
  116. end
  117. variant.description = unwrap(variant.description)
  118. for _, arg in ipairs(variant.arguments) do
  119. arg.description = unwrap(arg.description)
  120. processTable(arg.table)
  121. end
  122. for _, ret in ipairs(variant.returns) do
  123. ret.description = unwrap(ret.description)
  124. processTable(ret.table)
  125. end
  126. end
  127. fn.arguments = nil
  128. fn.returns = nil
  129. track(fn)
  130. return fn
  131. end
  132. local function processObject(path, parent)
  133. local object = require(path .. '.init')
  134. assert(type(object) == 'table', string.format('%s/init.lua did not return a table', path))
  135. object.key = path:match('[^/]+$')
  136. object.name = object.key
  137. object.description = unwrap(object.description)
  138. object.summary = object.summary or object.description
  139. object.module = parent.key
  140. object.constructors = pluralify(object, 'constructor')
  141. object.notes = unwrap(object.notes)
  142. object.examples = pluralify(object, 'example')
  143. if object.sections then
  144. for _, section in ipairs(object.sections) do
  145. section.description = unwrap(section.description)
  146. end
  147. end
  148. local methods = {}
  149. for _, file in ipairs(lovr.filesystem.getDirectoryItems(path)) do
  150. if file ~= 'init.lua' then
  151. local method = file:gsub('%..+$', '')
  152. local key = ('%s:%s'):format(object.name, method)
  153. methods[key] = processFunction(path .. '/' .. method, object)
  154. end
  155. end
  156. if object.methods then
  157. for i, key in ipairs(object.methods) do
  158. object.methods[i] = methods[key]
  159. warnIf(not methods[key], '%s links to unknown method %q', object.key, key)
  160. methods[key] = nil
  161. end
  162. for method in pairs(methods) do
  163. warn('%s is missing link to %q', object.key, method)
  164. end
  165. else
  166. object.methods = {}
  167. for key, method in pairs(methods) do
  168. table.insert(object.methods, method)
  169. end
  170. table.sort(object.methods, function(a, b) return a.key < b.key end)
  171. end
  172. for k, example in ipairs(object.examples or {}) do
  173. object.examples[k] = processExample(example, object.key)
  174. end
  175. track(object)
  176. return object
  177. end
  178. local function processModule(path)
  179. local module = require(path .. '.init') -- So we avoid requiring the module itself
  180. module.key = module.external and path:match('[^/]+$') or path:gsub('/', '.')
  181. module.name = module.external and module.key or module.key:match('[^%.]+$')
  182. module.description = unwrap(module.description)
  183. module.functions = {}
  184. module.objects = {}
  185. module.enums = {}
  186. module.notes = unwrap(module.notes)
  187. if module.sections then
  188. for _, section in ipairs(module.sections) do
  189. section.description = unwrap(section.description)
  190. end
  191. end
  192. module.examples = pluralify(module, 'example')
  193. for k, example in ipairs(module.examples or {}) do
  194. module.examples[k] = processExample(example, module.key)
  195. end
  196. for _, file in ipairs(lovr.filesystem.getDirectoryItems(path)) do
  197. local childPath = path .. '/' .. file
  198. local childModule = childPath:gsub('%..+$', '')
  199. local isFile = lovr.filesystem.isFile(childPath)
  200. local capitalized = file:match('^[A-Z]')
  201. if file ~= 'init.lua' and not capitalized and isFile then
  202. table.insert(module.functions, processFunction(childModule, module))
  203. elseif capitalized and not isFile then
  204. table.insert(module.objects, processObject(childModule, module))
  205. elseif capitalized and isFile then
  206. table.insert(module.enums, processEnum(childModule, module))
  207. end
  208. end
  209. table.sort(module.functions, function(a, b) return a.key < b.key end)
  210. table.sort(module.objects, function(a, b) return a.key < b.key end)
  211. table.sort(module.enums, function(a, b) return a.key < b.key end)
  212. track(module)
  213. return module
  214. end
  215. -- Validation
  216. local function validateRelated(item)
  217. for _, key in ipairs(item.related or {}) do
  218. warnIf(not lookup[key], '%s has unknown related item %s', item.key, key)
  219. warnIf(key == item.key, '%s should not be related to itself', key)
  220. end
  221. end
  222. local function validateEnum(enum)
  223. for i, value in ipairs(enum.values) do
  224. warnIf(not value.name, 'Enum %s value #%d is missing name', enum.name, i)
  225. warnIf(not value.description, 'Enum %s value #%d is missing description', enum.name, i)
  226. end
  227. validateRelated(enum)
  228. end
  229. local function validateFunction(fn)
  230. if fn.tag then
  231. local found = false
  232. for _, section in ipairs(lookup[fn.module].sections or {}) do
  233. if section.tag == fn.tag then found = true break end
  234. end
  235. for _, object in ipairs(lookup[fn.module].objects) do
  236. for _, section in ipairs(object.sections or {}) do
  237. if section.tag == fn.tag then found = true break end
  238. end
  239. end
  240. warnIf(not found, 'Unknown tag %s for %s', fn.tag, fn.key)
  241. end
  242. for _, variant in ipairs(fn.variants) do
  243. for _, arg in ipairs(variant.arguments) do
  244. warnIf(not arg or not arg.name, 'Invalid argument for variant of %s', fn.key)
  245. warnIf(not arg.type or (arg.type:match('^[A-Z]') and not lookup[arg.type]), 'Invalid or missing argument type in %s', fn.key)
  246. end
  247. for _, ret in ipairs(variant.returns) do
  248. warnIf(not ret or not ret.name, 'Invalid return for variant of %s', fn.key)
  249. warnIf(not ret.type or (ret.type:match('^[A-Z]') and not lookup[ret.type]), 'Invalid or missing return type in %s', fn.key)
  250. end
  251. end
  252. validateRelated(fn)
  253. end
  254. local function validateObject(object)
  255. for _, constructor in ipairs(object.constructors or {}) do
  256. warnIf(not lookup[constructor], 'Constructor for %s not found: %s', object.key, constructor)
  257. end
  258. for _, method in ipairs(object.methods or {}) do
  259. validateFunction(method)
  260. end
  261. local metatable = debug.getregistry()[object.name]
  262. if dev and metatable then
  263. local hasMethod = {}
  264. for _, method in ipairs(object.methods or {}) do
  265. warnIf(not metatable[method.name], '%s has docs for unknown method %s', object.name, method.name)
  266. hasMethod[method.name] = true
  267. end
  268. if object.extends then
  269. for i, method in ipairs(lookup[object.extends].methods) do
  270. hasMethod[method.name] = true
  271. end
  272. end
  273. local ignore = {
  274. type = true,
  275. release = true,
  276. monkey = true
  277. }
  278. for name in pairs(metatable) do
  279. if not name:match('^__') and not ignore[name] then
  280. warnIf(not hasMethod[name], '%s is missing docs for %s', object.name, name)
  281. end
  282. end
  283. end
  284. validateRelated(object)
  285. end
  286. local function validateModule(module)
  287. local t = lovr[module.name]
  288. for _, object in ipairs(module.objects) do
  289. validateObject(object)
  290. end
  291. for _, fn in ipairs(module.functions) do
  292. validateFunction(fn)
  293. if dev and not fn.deprecated then
  294. warnIf(t and not t[fn.name], '%s has docs for unknown function %s', module.key, fn.name)
  295. end
  296. end
  297. for _, fn in ipairs(module.enums) do
  298. validateEnum(fn)
  299. end
  300. end
  301. function lovr.load()
  302. local api = {
  303. modules = {},
  304. callbacks = {}
  305. }
  306. -- So errhand exits
  307. lovr.graphics = nil
  308. -- Modules
  309. table.insert(api.modules, processModule('lovr'))
  310. for _, file in ipairs(lovr.filesystem.getDirectoryItems('lovr')) do
  311. local path = 'lovr/' .. file
  312. if file ~= 'callbacks' and file:match('^[a-z]') and lovr.filesystem.isDirectory(path) then
  313. table.insert(api.modules, processModule(path))
  314. end
  315. end
  316. -- Callbacks
  317. local callbacks = 'lovr/callbacks'
  318. for _, file in ipairs(lovr.filesystem.getDirectoryItems(callbacks)) do
  319. table.insert(api.callbacks, processFunction(callbacks .. '/' .. file:gsub('%.lua', ''), api.modules[1]))
  320. end
  321. -- Validate
  322. for _, callback in ipairs(api.callbacks) do
  323. validateFunction(callback)
  324. end
  325. for _, module in ipairs(api.modules) do
  326. validateModule(module)
  327. end
  328. -- Sort
  329. table.sort(api.modules, function(a, b) return a.key < b.key end)
  330. table.sort(api.callbacks, function(a, b) return a.key < b.key end)
  331. -- Serialize
  332. local file = io.open(lovr.filesystem.getSource() .. '/init.lua', 'w')
  333. assert(file, 'Could not open init.lua for writing')
  334. local keyPriority = {
  335. name = 1,
  336. tag = 2,
  337. summary = 3,
  338. type = 4,
  339. description = 5,
  340. key = 6,
  341. module = 7,
  342. arguments = 8,
  343. returns = 9
  344. }
  345. local function sort(keys, t)
  346. table.sort(keys, function(lhs, rhs)
  347. local leftPrio = keyPriority[lhs]
  348. local rightPrio = keyPriority[rhs]
  349. if leftPrio and rightPrio then
  350. return leftPrio < rightPrio
  351. elseif leftPrio or rightPrio then
  352. return leftPrio ~= nil
  353. else
  354. return lhs < rhs
  355. end
  356. end)
  357. end
  358. local contents = 'return ' .. serpent.block(api, { comment = false, sortkeys = sort })
  359. file:write(contents)
  360. file:close()
  361. -- Bye
  362. lovr.event.quit()
  363. end