main.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  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. warnIf(not enum.summary, 'Enum %s is missing summary', enum.name)
  224. warnIf(not enum.description, 'Enum %s is missing description', enum.name)
  225. for i, value in ipairs(enum.values) do
  226. warnIf(not value.name, 'Enum %s value #%d is missing name', enum.name, i)
  227. warnIf(not value.description, 'Enum %s value #%d is missing description', enum.name, i)
  228. end
  229. validateRelated(enum)
  230. end
  231. local function validateType(type, fields, key, kind)
  232. if not type then
  233. warn('Missing %s type in %s', kind, key)
  234. elseif type:match('^[A-Z]') then
  235. warnIf(not lookup[type], 'Invalid %s type "%s" in %s', kind, type, key)
  236. else
  237. local valid = {
  238. boolean = true,
  239. number = true,
  240. table = true,
  241. string = true,
  242. userdata = true,
  243. lightuserdata = true,
  244. ['function'] = true,
  245. ['*'] = true
  246. }
  247. warnIf(not valid[type], 'Invalid %s type "%s" in %s', kind, type, key)
  248. if type == 'table' and fields then
  249. for i, field in ipairs(fields) do
  250. validateType(field.type, field.table, key, kind)
  251. end
  252. end
  253. end
  254. end
  255. local function validateFunction(fn)
  256. if fn.tag then
  257. local found = false
  258. for _, section in ipairs(lookup[fn.module].sections or {}) do
  259. if section.tag == fn.tag then found = true break end
  260. end
  261. for _, object in ipairs(lookup[fn.module].objects) do
  262. for _, section in ipairs(object.sections or {}) do
  263. if section.tag == fn.tag then found = true break end
  264. end
  265. end
  266. warnIf(not found, 'Unknown tag %s for %s', fn.tag, fn.key)
  267. end
  268. for _, variant in ipairs(fn.variants) do
  269. for _, arg in ipairs(variant.arguments) do
  270. warnIf(not arg or not arg.name, 'Invalid argument for variant of %s', fn.key)
  271. validateType(arg.type, arg.table, fn.key, 'argument')
  272. end
  273. for _, ret in ipairs(variant.returns) do
  274. warnIf(not ret or not ret.name, 'Invalid return for variant of %s', fn.key)
  275. validateType(ret.type, ret.table, fn.key, 'return')
  276. end
  277. end
  278. validateRelated(fn)
  279. end
  280. local function validateObject(object)
  281. for _, constructor in ipairs(object.constructors or {}) do
  282. warnIf(not lookup[constructor], 'Constructor for %s not found: %s', object.key, constructor)
  283. end
  284. for _, method in ipairs(object.methods or {}) do
  285. validateFunction(method)
  286. end
  287. local metatable = debug.getregistry()[object.name]
  288. if dev and metatable then
  289. local hasMethod = {}
  290. for _, method in ipairs(object.methods or {}) do
  291. warnIf(not metatable[method.name], '%s has docs for unknown method %s', object.name, method.name)
  292. hasMethod[method.name] = true
  293. end
  294. if object.extends then
  295. for i, method in ipairs(lookup[object.extends].methods) do
  296. hasMethod[method.name] = true
  297. end
  298. end
  299. local ignore = {
  300. type = true,
  301. release = true,
  302. monkey = true
  303. }
  304. for name in pairs(metatable) do
  305. if not name:match('^__') and not ignore[name] then
  306. warnIf(not hasMethod[name], '%s is missing docs for %s', object.name, name)
  307. end
  308. end
  309. end
  310. validateRelated(object)
  311. end
  312. local function validateModule(module)
  313. local t = lovr[module.name]
  314. for _, object in ipairs(module.objects) do
  315. validateObject(object)
  316. end
  317. for _, fn in ipairs(module.functions) do
  318. validateFunction(fn)
  319. if dev and not fn.deprecated then
  320. warnIf(t and not t[fn.name], '%s has docs for unknown function %s', module.key, fn.name)
  321. end
  322. end
  323. for _, fn in ipairs(module.enums) do
  324. validateEnum(fn)
  325. end
  326. end
  327. function lovr.load()
  328. local api = {
  329. modules = {},
  330. callbacks = {}
  331. }
  332. -- So errhand exits
  333. lovr.graphics = nil
  334. -- Modules
  335. table.insert(api.modules, processModule('lovr'))
  336. for _, file in ipairs(lovr.filesystem.getDirectoryItems('lovr')) do
  337. local path = 'lovr/' .. file
  338. if file ~= 'callbacks' and file:match('^[a-z]') and lovr.filesystem.isDirectory(path) then
  339. table.insert(api.modules, processModule(path))
  340. end
  341. end
  342. -- Callbacks
  343. local callbacks = 'lovr/callbacks'
  344. for _, file in ipairs(lovr.filesystem.getDirectoryItems(callbacks)) do
  345. table.insert(api.callbacks, processFunction(callbacks .. '/' .. file:gsub('%.lua', ''), api.modules[1]))
  346. end
  347. -- Validate
  348. for _, callback in ipairs(api.callbacks) do
  349. validateFunction(callback)
  350. end
  351. for _, module in ipairs(api.modules) do
  352. validateModule(module)
  353. end
  354. -- Sort
  355. table.sort(api.modules, function(a, b) return a.key < b.key end)
  356. table.sort(api.callbacks, function(a, b) return a.key < b.key end)
  357. -- Serialize
  358. local file = io.open(lovr.filesystem.getSource() .. '/init.lua', 'w')
  359. assert(file, 'Could not open init.lua for writing')
  360. local keyPriority = {
  361. name = 1,
  362. tag = 2,
  363. summary = 3,
  364. type = 4,
  365. description = 5,
  366. key = 6,
  367. module = 7,
  368. arguments = 8,
  369. returns = 9
  370. }
  371. local function sort(keys, t)
  372. table.sort(keys, function(lhs, rhs)
  373. local leftPrio = keyPriority[lhs]
  374. local rightPrio = keyPriority[rhs]
  375. if leftPrio and rightPrio then
  376. return leftPrio < rightPrio
  377. elseif leftPrio or rightPrio then
  378. return leftPrio ~= nil
  379. else
  380. return lhs < rhs
  381. end
  382. end)
  383. end
  384. local contents = 'return ' .. serpent.block(api, { comment = false, sortkeys = sort })
  385. file:write(contents)
  386. file:close()
  387. -- Bye
  388. lovr.event.quit()
  389. end