main.lua 14 KB

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