main.lua 14 KB

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