Merge: src/doc/commands: clean commands hierarchy
[nit.git] / src / doc / commands / commands_parser.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # A parser that create DocCommand from a string
16 #
17 # Used by both Nitx and the Markdown doc commands.
18 module commands_parser
19
20 import commands_catalog
21 import commands_graph
22 import commands_ini
23 import commands_main
24 import commands_usage
25
26 # Parse string commands to create DocQueries
27 class CommandParser
28
29 # Model used to retrieve mentities
30 var model: Model
31
32 # Main module for linearization
33 var mainmodule: MModule
34
35 # ModelBuilder used to retrieve AST nodes
36 var modelbuilder: ModelBuilder
37
38 # Catalog used for catalog commands
39 var catalog: nullable Catalog
40
41 # List of allowed command names for this parser
42 var allowed_commands: Array[String] = [
43 "link", "doc", "code", "lin", "uml", "graph", "search",
44 "parents", "ancestors", "children", "descendants",
45 "param", "return", "new", "call", "defs", "list", "random",
46 "ini-desc", "ini-git", "ini-issues", "ini-maintainer", "ini-contributors", "ini-license",
47 "license-file", "contrib-file", "license-content", "contrib-content", "git-clone",
48 "mains", "main-compile", "main-run", "main-opts", "testing",
49 "catalog", "stats", "tags", "tag", "person", "contrib", "maintain"] is writable
50
51 # List of commands usage and documentation
52 var commands_usage: Map[String, String] do
53 var usage = new ArrayMap[String, String]
54 usage["search: <string>"] = "list entities matching `string`"
55 usage["link: <name>"] = "display the link to `name`"
56 usage["doc: <name>"] = "display the documentation for `name`"
57 usage["defs: <name>"] = "list all definitions for `name`"
58 usage["code: <name>"] = "display the code for `name`"
59 usage["lin: <name>"] = "display the linearization for `name`"
60 usage["uml: <name>"] = "display the UML diagram for `name`"
61 usage["graph: <name>"] = "display the inheritance graph for `name`"
62 usage["parents: <name>"] = "list the direct parents of `name`"
63 usage["ancestors: <name>"] = "list all ancestors of `name`"
64 usage["children: <name>"] = "list direct children of `name`"
65 usage["descendants: <name>"] = "list all descendants of `name`"
66 usage["param: <type>"] = "list all methods accepting `type` as parameter"
67 usage["return: <type>"] = "list all methods returning `type`"
68 usage["new: <class>"] = "list all methods initializing `class`"
69 usage["call: <property>"] = "list all methods calling `property`"
70 usage["list: <kind>"] = "list all entities of `kind` from the model"
71 usage["random: <kind>"] = "list random entities of `kind` from the model"
72 usage["catalog:"] = "list packages from catalog"
73 usage["stats:"] = "display catalog statistics"
74 usage["tags:"] = "list all tabs from catalog"
75 usage["tag: <tag>"] = "list all packages with `tag`"
76 usage["maintain: <person>"] = "list all packages maintained by `person`"
77 usage["contrib: <person>"] = "list all packages contributed by `person`"
78 # Ini commands
79 usage["ini-desc: <package>"] = "display the description from the `package` ini file"
80 usage["ini-git: <package>"] = "display the git url from the `package` ini file"
81 usage["ini-issues: <package>"] = "display the issues url from the `package` ini file"
82 usage["ini-license: <package>"] = "display the license from the `package` ini file"
83 usage["ini-maintainer: <package>"] = "display the maintainer from the `package` ini file"
84 usage["ini-contributors: <package>"] = "display the contributors from the `package` ini file"
85 usage["license-file: <package>"] = "display the license file for the `package`"
86 usage["license-content: <package>"] = "display the license file content for the `package`"
87 usage["contrib-file: <package>"] = "display the contrib file for the `package`"
88 usage["contrib-content: <package>"] = "display the contrib file content for the `package`"
89 usage["git-clone: <package>"] = "display the git clone command for the `package`"
90 # Main
91 usage["mains: <name>"] = "display the list of main methods for `name`"
92 usage["main-compile: <name>"] = "display the nitc command to compile `name`"
93 usage["main-run: <name>"] = "display the command to run `name`"
94 usage["main-opts: <name>"] = "display the command options for `name`"
95 usage["testing: <name>"] = "display the nitunit command to test `name`"
96 return usage
97 end
98
99 # Parse `string` as a DocCommand
100 #
101 # Returns `null` if the string cannot be parsed.
102 # See `error` for the error messages produced by both the parser and the commands.
103 fun parse(string: String): nullable DocCommand do
104 var pos = 0
105 var tmp = new FlatBuffer
106 error = null
107
108 # Parse command name
109 pos = string.read_until(tmp, pos, ':', '|')
110 var name = tmp.write_to_string.trim
111
112 # Check allowed commands
113 if name.is_empty then
114 error = new CmdParserError("Empty command name", 0)
115 return null
116 end
117 # If the command name contains two consecutive colons or there is no colon in the name,
118 # we certainly have a wiki link to a mentity
119 var is_short_link = false
120 if (pos < string.length - 2 and string[pos] == ':' and string[pos + 1] == ':') or
121 pos == string.length then
122 is_short_link = true
123 else if pos < string.length - 1 and string[pos] == '|' then
124 is_short_link = true
125 pos -= 1
126 else if not allowed_commands.has(name) then
127 error = new CmdParserError("Unknown command name `{name}`", 0)
128 return null
129 end
130
131 # Parse the argument
132 tmp.clear
133 pos = string.read_until(tmp, pos + 1, '|')
134 var arg = tmp.write_to_string.trim
135 if is_short_link and not arg.is_empty then
136 arg = "{name}:{arg}"
137 else if is_short_link then
138 arg = name
139 end
140
141 # Parse command options
142 var opts = new CmdOptions
143 while pos < string.length do
144 # Parse option name
145 tmp.clear
146 pos = string.read_until(tmp, pos + 1, ':', ',')
147 var oname = tmp.write_to_string.trim
148 var oval = ""
149 if oname.is_empty then break
150 # Parse option value
151 if pos < string.length and string[pos] == ':' then
152 tmp.clear
153 pos = string.read_until(tmp, pos + 1, ',')
154 oval = tmp.write_to_string.trim
155 end
156 opts[oname] = oval
157 end
158
159 # Build the command
160 var command
161 if is_short_link then
162 command = new CmdEntityLink(model)
163 else
164 command = new_command(name)
165 end
166 if command == null then
167 error = new CmdParserError("Unknown command name `{name}`", 0)
168 return null
169 end
170
171 # Initialize command from string options
172 var status = command.parser_init(arg, opts)
173 if not status isa CmdSuccess then error = status
174
175 return command
176 end
177
178 # Init a new DocCommand from its `name`
179 #
180 # You must redefine this method to add new custom commands.
181 fun new_command(name: String): nullable DocCommand do
182 # CmdEntity
183 if name == "link" then return new CmdEntityLink(model)
184 if name == "doc" then return new CmdComment(model)
185 if name == "code" then return new CmdEntityCode(model, modelbuilder)
186 if name == "lin" then return new CmdLinearization(model, mainmodule)
187 if name == "defs" then return new CmdFeatures(model)
188 if name == "parents" then return new CmdParents(model, mainmodule)
189 if name == "ancestors" then return new CmdAncestors(model, mainmodule)
190 if name == "children" then return new CmdChildren(model, mainmodule)
191 if name == "descendants" then return new CmdDescendants(model, mainmodule)
192 if name == "param" then return new CmdParam(model)
193 if name == "return" then return new CmdReturn(model)
194 if name == "new" then return new CmdNew(model, modelbuilder)
195 if name == "call" then return new CmdCall(model, modelbuilder)
196 # CmdGraph
197 if name == "uml" then return new CmdUML(model, mainmodule)
198 if name == "graph" then return new CmdInheritanceGraph(model, mainmodule)
199 # CmdModel
200 if name == "list" then return new CmdModelEntities(model)
201 if name == "random" then return new CmdRandomEntities(model)
202 # Ini
203 if name == "ini-desc" then return new CmdIniDescription(model)
204 if name == "ini-git" then return new CmdIniGitUrl(model)
205 if name == "ini-issues" then return new CmdIniIssuesUrl(model)
206 if name == "ini-license" then return new CmdIniLicense(model)
207 if name == "ini-maintainer" then return new CmdIniMaintainer(model)
208 if name == "ini-contributors" then return new CmdIniContributors(model)
209 if name == "license-file" then return new CmdLicenseFile(model)
210 if name == "license-content" then return new CmdLicenseFileContent(model)
211 if name == "contrib-file" then return new CmdContribFile(model)
212 if name == "contrib-content" then return new CmdContribFileContent(model)
213 if name == "git-clone" then return new CmdIniCloneCommand(model)
214 # CmdMain
215 if name == "mains" then return new CmdMains(model)
216 if name == "main-compile" then return new CmdMainCompile(model)
217 if name == "main-run" then return new CmdManSynopsis(model)
218 if name == "main-opts" then return new CmdManOptions(model)
219 if name == "testing" then return new CmdTesting(model)
220 # CmdCatalog
221 var catalog = self.catalog
222 if catalog != null then
223 if name == "catalog" then return new CmdCatalogPackages(model, catalog)
224 if name == "stats" then return new CmdCatalogStats(model, catalog)
225 if name == "tags" then return new CmdCatalogTags(model, catalog)
226 if name == "tag" then return new CmdCatalogTag(model, catalog)
227 if name == "person" then return new CmdCatalogPerson(model, catalog)
228 if name == "contrib" then return new CmdCatalogContributing(model, catalog)
229 if name == "maintain" then return new CmdCatalogMaintaining(model, catalog)
230 if name == "search" then return new CmdCatalogSearch(model, catalog)
231 else
232 if name == "search" then return new CmdSearch(model)
233 end
234 return null
235 end
236
237 # Error or warning from last call to `parse`
238 var error: nullable CmdMessage = null
239 end
240
241 # An error produced by the CmdParser
242 class CmdParserError
243 super CmdError
244
245 # Error message
246 var message: String
247
248 # Column related to the error
249 var column: nullable Int
250
251 redef fun to_s do return message
252 end
253
254 redef class DocCommand
255
256 # Initialize the command from the CommandParser data
257 fun parser_init(arg: String, options: CmdOptions): CmdMessage do
258 var filter = cmd_filter
259 var opt_vis = options.opt_visibility("min-visibility")
260 if opt_vis != null then filter.min_visibility = opt_vis
261 var opt_fictive = options.opt_bool("no-fictive")
262 if opt_fictive != null then filter.accept_fictive = not opt_fictive
263 var opt_test = options.opt_bool("no-test")
264 if opt_test != null then filter.accept_test = not opt_test
265 var opt_redef = options.opt_bool("no-redef")
266 if opt_redef != null then filter.accept_redef = not opt_redef
267 var opt_extern = options.opt_bool("no-extern")
268 if opt_extern != null then filter.accept_extern = not opt_extern
269 var opt_example = options.opt_bool("no-example")
270 if opt_example != null then filter.accept_example = not opt_example
271 var opt_attr = options.opt_bool("no-attribute")
272 if opt_attr != null then filter.accept_attribute = not opt_attr
273 var opt_doc = options.opt_bool("no-empty-doc")
274 if opt_doc != null then filter.accept_empty_doc = not opt_doc
275 var opt_inh = options.opt_mentity(model, "inherit")
276 if opt_inh != null then filter.accept_inherited = opt_inh
277 var opt_match = options.opt_string("match")
278 if opt_match != null then filter.accept_full_name = opt_match
279 self.filter = filter
280 return init_command
281 end
282 end
283
284 redef class CmdEntity
285 redef fun parser_init(mentity_name, options) do
286 self.mentity_name = mentity_name
287 return super
288 end
289 end
290
291 redef class CmdList
292 redef fun parser_init(mentity_name, options) do
293 var opt_page = options.opt_int("page")
294 if opt_page != null then page = opt_page
295 var opt_limit = options.opt_int("limit")
296 if opt_limit != null then limit = opt_limit
297 return super
298 end
299 end
300
301 # Model commands
302
303 redef class CmdComment
304 redef fun parser_init(mentity_name, options) do
305 var opt_full_doc = options.opt_bool("only-synopsis")
306 if opt_full_doc != null then full_doc = not opt_full_doc
307 var opt_fallback = options.opt_bool("no-fallback")
308 if opt_fallback != null then fallback = not opt_fallback
309 var opt_format = options.opt_string("format")
310 if opt_format != null then format = opt_format
311 return super
312 end
313 end
314
315 redef class CmdEntityLink
316 redef fun parser_init(mentity_name, options) do
317 var opt_text = options.opt_string("text")
318 if opt_text != null then text = opt_text
319 var opt_title = options.opt_string("title")
320 if opt_title != null then title = opt_title
321 return super
322 end
323 end
324
325 redef class CmdCode
326 redef fun parser_init(mentity_name, options) do
327 var opt_format = options.opt_string("format")
328 if opt_format != null then format = opt_format
329 return super
330 end
331 end
332
333 redef class CmdSearch
334 redef fun parser_init(mentity_name, options) do
335 query = mentity_name
336 return super
337 end
338 end
339
340 redef class CmdAncestors
341 redef fun parser_init(mentity_name, options) do
342 var opt_parents = options.opt_bool("no-parents")
343 if opt_parents != null then parents = not opt_parents
344 return super
345 end
346 end
347
348 redef class CmdDescendants
349 redef fun parser_init(mentity_name, options) do
350 var opt_children = options.opt_bool("no-children")
351 if opt_children != null then children = not opt_children
352 return super
353 end
354 end
355
356 redef class CmdModelEntities
357 redef fun parser_init(kind, options) do
358 self.kind = kind
359 return super
360 end
361 end
362
363 redef class CmdGraph
364 redef fun parser_init(mentity_name, options) do
365 var opt_format = options.opt_string("format")
366 if opt_format != null then format = opt_format
367 return super
368 end
369 end
370
371 redef class CmdInheritanceGraph
372 redef fun parser_init(mentity_name, options) do
373 var opt_pdepth = options.opt_int("pdepth")
374 if opt_pdepth != null then pdepth = opt_pdepth
375 var opt_cdepth = options.opt_int("cdepth")
376 if opt_cdepth != null then cdepth = opt_cdepth
377 return super
378 end
379 end
380
381 # Catalog commands
382
383 redef class CmdCatalogTag
384 redef fun parser_init(mentity_name, options) do
385 tag = mentity_name
386 return super
387 end
388 end
389
390 redef class CmdCatalogPerson
391 redef fun parser_init(mentity_name, options) do
392 person_name = mentity_name
393 return super
394 end
395 end
396
397 # Utils
398
399 # Commands options
400 class CmdOptions
401 super HashMap[String, String]
402
403 # Map String visiblity name to MVisibility object
404 var allowed_visibility: HashMap[String, MVisibility] is lazy do
405 var res = new HashMap[String, MVisibility]
406 res["public"] = public_visibility
407 res["protected"] = protected_visibility
408 res["private"] = private_visibility
409 return res
410 end
411
412 # Get option value for `key` as String
413 #
414 # Return `null` if no option with that `key` or if value is empty.
415 fun opt_string(key: String): nullable String do
416 if not has_key(key) then return null
417 var value = self[key]
418 if value.is_empty then return null
419 return value
420 end
421
422 # Get option value for `key` as Int
423 #
424 # Return `null` if no option with that `key` or if value is not an Int.
425 fun opt_int(key: String): nullable Int do
426 if not has_key(key) then return null
427 var value = self[key]
428 if not value.is_int then return null
429 return value.to_i
430 end
431
432 # Get option value as bool
433 #
434 # Return `true` if the value with that `key` is empty or equals `"true"`.
435 # Return `false` if the value with that `key` equals `"false"`.
436 # Return `null` in any other case.
437 fun opt_bool(key: String): nullable Bool do
438 if not has_key(key) then return null
439 var value = self[key]
440 if value.is_empty or value == "true" then return true
441 if value == "false" then return false
442 return null
443 end
444
445 # Get option as a MVisibility
446 #
447 # Return `null` if no option with that `key` or if the value is not in
448 # `allowed_visibility`.
449 fun opt_visibility(key: String): nullable MVisibility do
450 var value = opt_string(key)
451 if value == null then return null
452 if not allowed_visibility.keys.has(key) then return null
453 return allowed_visibility[value]
454 end
455
456 # Get option as a MEntity
457 #
458 # Lookup first by `MEntity::full_name` then by `MEntity::name`.
459 # Return `null` if the mentity name does not exist or return a conflict.
460 private fun opt_mentity(model: Model, key: String): nullable MEntity do
461 var value = opt_string(key)
462 if value == null or value.is_empty then return null
463
464 var mentity = model.mentity_by_full_name(value)
465 if mentity != null then return mentity
466
467 var mentities = model.mentities_by_name(value)
468 if mentities.is_empty or mentities.length > 1 then return null
469 return mentities.first
470 end
471 end
472
473 redef class Text
474 # Read `self` as raw text until `nend` and append it to the `out` buffer.
475 private fun read_until(out: FlatBuffer, start: Int, nend: Char...): Int do
476 var pos = start
477 while pos < length do
478 var c = self[pos]
479 var end_reached = false
480 for n in nend do
481 if c == n then
482 end_reached = true
483 break
484 end
485 end
486 if end_reached then break
487 out.add c
488 pos += 1
489 end
490 return pos
491 end
492 end