src/doc/templates: protect access to nullable signatures
[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::commands_model
21 import commands::commands_graph
22 import commands::commands_usage
23 import commands::commands_catalog
24 import commands::commands_ini
25 import commands::commands_main
26
27 # Parse string commands to create DocQueries
28 class CommandParser
29
30 # Model used to retrieve mentities
31 var model: Model
32
33 # Main module for linearization
34 var mainmodule: MModule
35
36 # ModelBuilder used to retrieve AST nodes
37 var modelbuilder: ModelBuilder
38
39 # Catalog used for catalog commands
40 var catalog: nullable Catalog
41
42 # Filter to apply on model if any
43 var filter: nullable ModelFilter
44
45 # List of allowed command names for this parser
46 var allowed_commands: Array[String] = [
47 "link", "doc", "code", "lin", "uml", "graph", "search",
48 "parents", "ancestors", "children", "descendants",
49 "param", "return", "new", "call", "defs", "list", "random",
50 "ini-desc", "ini-git", "ini-issues", "ini-maintainer", "ini-contributors", "ini-license",
51 "license-file", "contrib-file", "license-content", "contrib-content", "git-clone",
52 "mains", "main-compile", "main-run", "main-opts", "testing",
53 "catalog", "stats", "tags", "tag", "person", "contrib", "maintain"] is writable
54
55 # List of commands usage and documentation
56 var commands_usage: Map[String, String] do
57 var usage = new ArrayMap[String, String]
58 usage["search: <string>"] = "list entities matching `string`"
59 usage["link: <name>"] = "display the link to `name`"
60 usage["doc: <name>"] = "display the documentation for `name`"
61 usage["defs: <name>"] = "list all definitions for `name`"
62 usage["code: <name>"] = "display the code for `name`"
63 usage["lin: <name>"] = "display the linearization for `name`"
64 usage["uml: <name>"] = "display the UML diagram for `name`"
65 usage["graph: <name>"] = "display the inheritance graph for `name`"
66 usage["parents: <name>"] = "list the direct parents of `name`"
67 usage["ancestors: <name>"] = "list all ancestors of `name`"
68 usage["children: <name>"] = "list direct children of `name`"
69 usage["descendants: <name>"] = "list all descendants of `name`"
70 usage["param: <type>"] = "list all methods accepting `type` as parameter"
71 usage["return: <type>"] = "list all methods returning `type`"
72 usage["new: <class>"] = "list all methods initializing `class`"
73 usage["call: <property>"] = "list all methods calling `property`"
74 usage["list: <kind>"] = "list all entities of `kind` from the model"
75 usage["random: <kind>"] = "list random entities of `kind` from the model"
76 usage["catalog:"] = "list packages from catalog"
77 usage["stats:"] = "display catalog statistics"
78 usage["tags:"] = "list all tabs from catalog"
79 usage["tag: <tag>"] = "list all packages with `tag`"
80 usage["maintain: <person>"] = "list all packages maintained by `person`"
81 usage["contrib: <person>"] = "list all packages contributed by `person`"
82 # Ini commands
83 usage["ini-desc: <package>"] = "display the description from the `package` ini file"
84 usage["ini-git: <package>"] = "display the git url from the `package` ini file"
85 usage["ini-issues: <package>"] = "display the issues url from the `package` ini file"
86 usage["ini-license: <package>"] = "display the license from the `package` ini file"
87 usage["ini-maintainer: <package>"] = "display the maintainer from the `package` ini file"
88 usage["ini-contributors: <package>"] = "display the contributors from the `package` ini file"
89 usage["license-file: <package>"] = "display the license file for the `package`"
90 usage["license-content: <package>"] = "display the license file content for the `package`"
91 usage["contrib-file: <package>"] = "display the contrib file for the `package`"
92 usage["contrib-content: <package>"] = "display the contrib file content for the `package`"
93 usage["git-clone: <package>"] = "display the git clone command for the `package`"
94 # Main
95 usage["mains: <name>"] = "display the list of main methods for `name`"
96 usage["main-compile: <name>"] = "display the nitc command to compile `name`"
97 usage["main-run: <name>"] = "display the command to run `name`"
98 usage["main-opts: <name>"] = "display the command options for `name`"
99 usage["testing: <name>"] = "display the nitunit command to test `name`"
100 return usage
101 end
102
103 # Parse `string` as a DocCommand
104 #
105 # Returns `null` if the string cannot be parsed.
106 # See `error` for the error messages produced by both the parser and the commands.
107 fun parse(string: String): nullable DocCommand do
108 var pos = 0
109 var tmp = new FlatBuffer
110 error = null
111
112 # Parse command name
113 pos = string.read_until(tmp, pos, ':', '|')
114 var name = tmp.write_to_string.trim
115
116 # Check allowed commands
117 if name.is_empty then
118 error = new CmdParserError("Empty command name", 0)
119 return null
120 end
121 # If the command name contains two consecutive colons or there is no colon in the name,
122 # we certainly have a wiki link to a mentity
123 var is_short_link = false
124 if (pos < string.length - 2 and string[pos] == ':' and string[pos + 1] == ':') or
125 pos == string.length then
126 is_short_link = true
127 else if pos < string.length - 1 and string[pos] == '|' then
128 is_short_link = true
129 pos -= 1
130 else if not allowed_commands.has(name) then
131 error = new CmdParserError("Unknown command name `{name}`", 0)
132 return null
133 end
134
135 # Parse the argument
136 tmp.clear
137 pos = string.read_until(tmp, pos + 1, '|')
138 var arg = tmp.write_to_string.trim
139 if is_short_link and not arg.is_empty then
140 arg = "{name}:{arg}"
141 else if is_short_link then
142 arg = name
143 end
144
145 # Parse command options
146 var opts = new HashMap[String, String]
147 while pos < string.length do
148 # Parse option name
149 tmp.clear
150 pos = string.read_until(tmp, pos + 1, ':', ',')
151 var oname = tmp.write_to_string.trim
152 var oval = ""
153 if oname.is_empty then break
154 # Parse option value
155 if pos < string.length and string[pos] == ':' then
156 tmp.clear
157 pos = string.read_until(tmp, pos + 1, ',')
158 oval = tmp.write_to_string.trim
159 end
160 opts[oname] = oval
161 end
162
163 # Build the command
164 var command
165 if is_short_link then
166 command = new CmdEntityLink(model, filter)
167 else
168 command = new_command(name)
169 end
170 if command == null then
171 error = new CmdParserError("Unknown command name `{name}`", 0)
172 return null
173 end
174
175 # Initialize command from string options
176 var status = command.parser_init(arg, opts)
177 if not status isa CmdSuccess then error = status
178
179 return command
180 end
181
182 # Init a new DocCommand from its `name`
183 #
184 # You must redefine this method to add new custom commands.
185 fun new_command(name: String): nullable DocCommand do
186 # CmdEntity
187 if name == "link" then return new CmdEntityLink(model, filter)
188 if name == "doc" then return new CmdComment(model, filter)
189 if name == "code" then return new CmdEntityCode(model, modelbuilder, filter)
190 if name == "lin" then return new CmdLinearization(model, mainmodule, filter)
191 if name == "defs" then return new CmdFeatures(model, filter)
192 if name == "parents" then return new CmdParents(model, mainmodule, filter)
193 if name == "ancestors" then return new CmdAncestors(model, mainmodule, filter)
194 if name == "children" then return new CmdChildren(model, mainmodule, filter)
195 if name == "descendants" then return new CmdDescendants(model, mainmodule, filter)
196 if name == "param" then return new CmdParam(model, filter)
197 if name == "return" then return new CmdReturn(model, filter)
198 if name == "new" then return new CmdNew(model, modelbuilder, filter)
199 if name == "call" then return new CmdCall(model, modelbuilder, filter)
200 # CmdGraph
201 if name == "uml" then return new CmdUML(model, mainmodule, filter)
202 if name == "graph" then return new CmdInheritanceGraph(model, mainmodule, filter)
203 # CmdModel
204 if name == "list" then return new CmdModelEntities(model, filter)
205 if name == "random" then return new CmdRandomEntities(model, filter)
206 # Ini
207 if name == "ini-desc" then return new CmdIniDescription(model, filter)
208 if name == "ini-git" then return new CmdIniGitUrl(model, filter)
209 if name == "ini-issues" then return new CmdIniIssuesUrl(model, filter)
210 if name == "ini-license" then return new CmdIniLicense(model, filter)
211 if name == "ini-maintainer" then return new CmdIniMaintainer(model, filter)
212 if name == "ini-contributors" then return new CmdIniContributors(model, filter)
213 if name == "license-file" then return new CmdLicenseFile(model, filter)
214 if name == "license-content" then return new CmdLicenseFileContent(model, filter)
215 if name == "contrib-file" then return new CmdContribFile(model, filter)
216 if name == "contrib-content" then return new CmdContribFileContent(model, filter)
217 if name == "git-clone" then return new CmdIniCloneCommand(model, filter)
218 # CmdMain
219 if name == "mains" then return new CmdMains(model, filter)
220 if name == "main-compile" then return new CmdMainCompile(model, filter)
221 if name == "main-run" then return new CmdManSynopsis(model, filter)
222 if name == "main-opts" then return new CmdManOptions(model, filter)
223 if name == "testing" then return new CmdTesting(model, filter)
224 # CmdCatalog
225 var catalog = self.catalog
226 if catalog != null then
227 if name == "catalog" then return new CmdCatalogPackages(model, catalog, filter)
228 if name == "stats" then return new CmdCatalogStats(model, catalog, filter)
229 if name == "tags" then return new CmdCatalogTags(model, catalog, filter)
230 if name == "tag" then return new CmdCatalogTag(model, catalog, filter)
231 if name == "person" then return new CmdCatalogPerson(model, catalog, filter)
232 if name == "contrib" then return new CmdCatalogContributing(model, catalog, filter)
233 if name == "maintain" then return new CmdCatalogMaintaining(model, catalog, filter)
234 if name == "search" then return new CmdCatalogSearch(model, catalog, filter)
235 else
236 if name == "search" then return new CmdSearch(model, filter)
237 end
238 return null
239 end
240
241 # Error or warning from last call to `parse`
242 var error: nullable CmdMessage = null
243 end
244
245 # An error produced by the CmdParser
246 class CmdParserError
247 super CmdError
248
249 # Error message
250 var message: String
251
252 # Column related to the error
253 var column: nullable Int
254
255 redef fun to_s do return message
256 end
257
258 redef class DocCommand
259
260 # Initialize the command from the CommandParser data
261 fun parser_init(arg: String, options: Map[String, String]): CmdMessage do
262 return init_command
263 end
264 end
265
266 redef class CmdEntity
267 redef fun parser_init(mentity_name, options) do
268 self.mentity_name = mentity_name
269 return super
270 end
271 end
272
273 redef class CmdList
274 redef fun parser_init(mentity_name, options) do
275 if options.has_key("limit") and options["limit"].is_int then limit = options["limit"].to_i
276 return super
277 end
278 end
279
280 # Model commands
281
282 redef class CmdComment
283 redef fun parser_init(mentity_name, options) do
284 full_doc = not options.has_key("only-synopsis")
285 fallback = not options.has_key("no-fallback")
286 if options.has_key("format") then format = options["format"]
287 return super
288 end
289 end
290
291 redef class CmdEntityLink
292 redef fun parser_init(mentity_name, options) do
293 if options.has_key("text") then text = options["text"]
294 if options.has_key("title") then title = options["title"]
295 return super
296 end
297 end
298
299 redef class CmdCode
300 redef fun parser_init(mentity_name, options) do
301 if options.has_key("format") then format = options["format"]
302 return super
303 end
304 end
305
306 redef class CmdSearch
307 redef fun parser_init(mentity_name, options) do
308 query = mentity_name
309 if options.has_key("page") and options["page"].is_int then page = options["page"].to_i
310 return super
311 end
312 end
313
314 redef class CmdAncestors
315 redef fun parser_init(mentity_name, options) do
316 if options.has_key("parents") and options["parents"] == "false" then parents = false
317 return super
318 end
319 end
320
321 redef class CmdDescendants
322 redef fun parser_init(mentity_name, options) do
323 if options.has_key("children") and options["children"] == "false" then children = false
324 return super
325 end
326 end
327
328 redef class CmdModelEntities
329 redef fun parser_init(kind, options) do
330 self.kind = kind
331 return super
332 end
333 end
334
335 redef class CmdGraph
336 redef fun parser_init(mentity_name, options) do
337 if options.has_key("format") then format = options["format"]
338 return super
339 end
340 end
341
342 redef class CmdInheritanceGraph
343 redef fun parser_init(mentity_name, options) do
344 if options.has_key("pdepth") and options["pdepth"].is_int then
345 pdepth = options["pdepth"].to_i
346 end
347 if options.has_key("cdepth") and options["cdepth"].is_int then
348 cdepth = options["cdepth"].to_i
349 end
350 return super
351 end
352 end
353
354 # Catalog commands
355
356 redef class CmdCatalogTag
357 redef fun parser_init(mentity_name, options) do
358 tag = mentity_name
359 return super
360 end
361 end
362
363 redef class CmdCatalogPerson
364 redef fun parser_init(mentity_name, options) do
365 person_name = mentity_name
366 return super
367 end
368 end
369
370 # Utils
371
372 redef class Text
373 # Read `self` as raw text until `nend` and append it to the `out` buffer.
374 private fun read_until(out: FlatBuffer, start: Int, nend: Char...): Int do
375 var pos = start
376 while pos < length do
377 var c = self[pos]
378 var end_reached = false
379 for n in nend do
380 if c == n then
381 end_reached = true
382 break
383 end
384 end
385 if end_reached then break
386 out.add c
387 pos += 1
388 end
389 return pos
390 end
391 end