From: Alexandre Terrasa Date: Tue, 24 Oct 2017 03:27:16 +0000 (-0400) Subject: doc/commands: introduce commands parser X-Git-Url: http://nitlanguage.org doc/commands: introduce commands parser Signed-off-by: Alexandre Terrasa --- diff --git a/src/doc/commands/commands_parser.nit b/src/doc/commands/commands_parser.nit new file mode 100644 index 0000000..af75cbe --- /dev/null +++ b/src/doc/commands/commands_parser.nit @@ -0,0 +1,286 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# A parser that create DocCommand from a string +# +# Used by both Nitx and the Markdown doc commands. +module commands_parser + +import commands::commands_model +import commands::commands_graph +import commands::commands_usage +import commands::commands_catalog + +# Parse string commands to create DocQueries +class CommandParser + + # ModelView used to retrieve mentities + var view: ModelView + + # ModelBuilder used to retrieve AST nodes + var modelbuilder: ModelBuilder + + # Catalog used for catalog commands + var catalog: nullable Catalog + + # List of allowed command names for this parser + var allowed_commands: Array[String] = [ + "doc", "code", "lin", "uml", "graph", "search", + "parents", "ancestors", "children", "descendants", + "param", "return", "new", "call", "defs", "list", "random", + "catalog", "stats", "tags", "tag", "person", "contrib", "maintain"] is writable + + # Parse `string` as a DocCommand + # + # Returns `null` if the string cannot be parsed. + # See `error` for the error messages produced by both the parser and the commands. + fun parse(string: String): nullable DocCommand do + var pos = 0 + var tmp = new FlatBuffer + error = null + + # Parse command name + pos = string.read_until(tmp, pos, ':') + var name = tmp.write_to_string.trim + + # Check allowed commands + if name.is_empty then + error = new CmdParserError("empty command name", 0) + return null + end + if not allowed_commands.has(name) then + error = new CmdParserError("unknown command name", 0) + return null + end + + # Parse the argument + tmp.clear + pos = string.read_until(tmp, pos + 1, '|') + var arg = tmp.write_to_string.trim + + # Parse command options + var opts = new HashMap[String, String] + while pos < string.length do + # Parse option name + tmp.clear + pos = string.read_until(tmp, pos + 1, ':', ',') + var oname = tmp.write_to_string.trim + var oval = "" + if oname.is_empty then break + # Parse option value + if pos < string.length and string[pos] == ':' then + tmp.clear + pos = string.read_until(tmp, pos + 1, ',') + oval = tmp.write_to_string.trim + end + opts[oname] = oval + end + + # Build the command + var command = new_command(name) + if command == null then + error = new CmdParserError("Unknown command name") + return null + end + + # Initialize command from string options + var status = command.parser_init(arg, opts) + if not status isa CmdSuccess then error = status + + return command + end + + # Init a new DocCommand from its `name` + # + # You must redefine this method to add new custom commands. + fun new_command(name: String): nullable DocCommand do + # CmdEntity + if name == "doc" then return new CmdComment(view) + if name == "code" then return new CmdCode(view, modelbuilder) + if name == "lin" then return new CmdLinearization(view) + if name == "defs" then return new CmdFeatures(view) + if name == "parents" then return new CmdParents(view) + if name == "ancestors" then return new CmdAncestors(view) + if name == "children" then return new CmdChildren(view) + if name == "descendants" then return new CmdDescendants(view) + if name == "param" then return new CmdParam(view) + if name == "return" then return new CmdReturn(view) + if name == "new" then return new CmdNew(view, modelbuilder) + if name == "call" then return new CmdCall(view, modelbuilder) + # CmdGraph + if name == "uml" then return new CmdUML(view) + if name == "graph" then return new CmdInheritanceGraph(view) + # CmdModel + if name == "list" then return new CmdModelEntities(view) + if name == "random" then return new CmdRandomEntities(view) + # CmdCatalog + var catalog = self.catalog + if catalog != null then + if name == "catalog" then return new CmdCatalogPackages(view, catalog) + if name == "stats" then return new CmdCatalogStats(view, catalog) + if name == "tags" then return new CmdCatalogTags(view, catalog) + if name == "tag" then return new CmdCatalogTag(view, catalog) + if name == "person" then return new CmdCatalogPerson(view, catalog) + if name == "contrib" then return new CmdCatalogContributing(view, catalog) + if name == "maintain" then return new CmdCatalogMaintaining(view, catalog) + if name == "search" then return new CmdCatalogSearch(view, catalog) + else + if name == "search" then return new CmdSearch(view) + end + return null + end + + # Error or warning from last call to `parse` + var error: nullable CmdMessage = null +end + +# An error produced by the CmdParser +class CmdParserError + super CmdError + + # Error message + var message: String + + # Column related to the error + var column: nullable Int + + redef fun to_s do return message +end + +redef class DocCommand + + # Initialize the command from the CommandParser data + fun parser_init(arg: String, options: Map[String, String]): CmdMessage do + return init_command + end +end + +redef class CmdEntity + redef fun parser_init(mentity_name, options) do + self.mentity_name = mentity_name + return super + end +end + +redef class CmdList + redef fun parser_init(mentity_name, options) do + if options.has_key("limit") and options["limit"].is_int then limit = options["limit"].to_i + return super + end +end + +# Model commands + +redef class CmdComment + redef fun parser_init(mentity_name, options) do + full_doc = not options.has_key("only-synopsis") + fallback = not options.has_key("no-fallback") + if options.has_key("format") then format = options["format"] + return super + end +end + +redef class CmdCode + redef fun parser_init(mentity_name, options) do + if options.has_key("format") then format = options["format"] + return super + end +end + +redef class CmdSearch + redef fun parser_init(mentity_name, options) do + query = mentity_name + if options.has_key("page") and options["page"].is_int then page = options["page"].to_i + return super + end +end + +redef class CmdAncestors + redef fun parser_init(mentity_name, options) do + if options.has_key("parents") and options["parents"] == "false" then parents = false + return super + end +end + +redef class CmdDescendants + redef fun parser_init(mentity_name, options) do + if options.has_key("children") and options["children"] == "false" then children = false + return super + end +end + +redef class CmdModelEntities + redef fun parser_init(kind, options) do + self.kind = kind + return super + end +end + +redef class CmdGraph + redef fun parser_init(mentity_name, options) do + if options.has_key("format") then format = options["format"] + return super + end +end + +redef class CmdInheritanceGraph + redef fun parser_init(mentity_name, options) do + if options.has_key("pdepth") and options["pdepth"].is_int then + pdepth = options["pdepth"].to_i + end + if options.has_key("cdepth") and options["cdepth"].is_int then + cdepth = options["cdepth"].to_i + end + return super + end +end + +# Catalog commands + +redef class CmdCatalogTag + redef fun parser_init(mentity_name, options) do + tag = mentity_name + return super + end +end + +redef class CmdCatalogPerson + redef fun parser_init(mentity_name, options) do + person_name = mentity_name + return super + end +end + +# Utils + +redef class Text + # Read `self` as raw text until `nend` and append it to the `out` buffer. + private fun read_until(out: FlatBuffer, start: Int, nend: Char...): Int do + var pos = start + while pos < length do + var c = self[pos] + var end_reached = false + for n in nend do + if c == n then + end_reached = true + break + end + end + if end_reached then break + out.add c + pos += 1 + end + return pos + end +end diff --git a/src/doc/commands/tests/test_commands_parser.nit b/src/doc/commands/tests/test_commands_parser.nit new file mode 100644 index 0000000..e58a157 --- /dev/null +++ b/src/doc/commands/tests/test_commands_parser.nit @@ -0,0 +1,298 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module test_commands_parser is test + +import test_commands +import doc::commands::tests::test_commands_catalog +import doc::commands::commands_parser + +class TestCommandsParser + super TestCommandsCatalog + test + + # CmdEntity + + fun test_cmd_parser_comment is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("doc: test_prog::Character") + assert cmd isa CmdComment + assert parser.error == null + assert cmd.mdoc != null + end + + # CmdInheritance + + fun test_cmd_parser_parents is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("parents: test_prog::Warrior") + assert cmd isa CmdParents + assert parser.error == null + assert cmd.results.as(not null).length == 1 + end + + fun test_cmd_parser_ancestors is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("ancestors: test_prog::Warrior") + assert cmd isa CmdAncestors + assert parser.error == null + assert cmd.results.as(not null).length == 2 + end + + fun test_cmd_parser_ancestors_without_parents is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("ancestors: test_prog::Warrior | parents: false") + assert cmd isa CmdAncestors + assert parser.error == null + assert cmd.results.as(not null).length == 1 + end + + fun test_cmd_parser_children is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("children: test_prog::Career") + assert cmd isa CmdChildren + assert parser.error == null + assert cmd.results.as(not null).length == 3 + end + + fun test_cmd_parser_descendants is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("descendants: Object") + assert cmd isa CmdDescendants + assert parser.error == null + assert cmd.results.as(not null).length == 19 + end + + fun test_cmd_parser_descendants_without_children is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("descendants: Object | children: false") + assert cmd isa CmdDescendants + assert parser.error == null + print cmd.results.as(not null) + assert cmd.results.as(not null).length == 7 + end + + # CmdSearch + + fun test_cmd_parser_search is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("search: Caracter") + assert cmd isa CmdSearch + assert parser.error == null + assert cmd.results != null + end + + fun test_cmd_parser_search_limit is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("search: Caracter | limit: 2") + assert cmd isa CmdSearch + assert parser.error == null + assert cmd.results.as(not null).length == 2 + end + + # CmdFeatures + + fun test_cmd_parser_features is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("defs: test_prog::Character") + assert cmd isa CmdFeatures + assert parser.error == null + assert cmd.results != null + end + + fun test_cmd_parser_features_limit is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("defs: test_prog::Character | limit: 2") + assert cmd isa CmdFeatures + assert parser.error == null + assert cmd.results.as(not null).length == 2 + end + + # CmdLinearization + + fun test_cmd_parser_lin is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("lin: test_prog::Character") + assert cmd isa CmdLinearization + assert parser.error == null + assert cmd.results != null + end + + fun test_cmd_parser_lin_limit is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("lin: test_prog::Character | limit: 2") + assert cmd isa CmdLinearization + assert parser.error == null + assert cmd.results.as(not null).length == 2 + end + + # CmdCode + + fun test_cmd_parser_code is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("code: test_prog::Character") + assert cmd isa CmdCode + assert parser.error == null + assert cmd.node != null + end + + # CmdModel + + fun test_cmd_parser_mentities is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("list: modules") + assert cmd isa CmdModelEntities + assert parser.error == null + assert cmd.results != null + end + + fun test_cmd_parser_results_mentities is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("random: modules") + assert cmd isa CmdRandomEntities + assert parser.error == null + assert cmd.results != null + end + + # CmdGraph + + fun test_cmd_parser_uml is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("uml: test_prog::Career") + assert cmd isa CmdUML + assert parser.error == null + assert cmd.uml != null + end + + fun test_cmd_parser_inh_graph is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("graph: test_prog::Career") + assert cmd isa CmdInheritanceGraph + assert parser.error == null + assert cmd.graph != null + end + + fun test_cmd_parser_inh_graph_opts is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("graph: test_prog::Career | cdepth: 2, pdepth: 5") + assert cmd isa CmdInheritanceGraph + assert parser.error == null + assert cmd.graph != null + assert cmd.cdepth == 2 + assert cmd.pdepth == 5 + end + + # CmdUsage + + fun test_cmd_parser_new is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("new: test_prog::Career") + assert cmd isa CmdNew + assert parser.error == null + assert cmd.results != null + end + + fun test_cmd_parser_call is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("call: strength_bonus") + assert cmd isa CmdCall + assert parser.error == null + assert cmd.results != null + end + + fun test_cmd_parser_return is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("return: test_prog::Career") + assert cmd isa CmdReturn + assert parser.error == null + assert cmd.results != null + end + + fun test_cmd_parser_param is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("param: test_prog::Career") + assert cmd isa CmdParam + assert parser.error == null + assert cmd.results != null + end + + # CmdCatalog + + fun test_parser_catalog_search is test do + var parser = new CommandParser(test_view, test_builder) + var cmd = parser.parse("search: Caracter") + assert cmd isa CmdSearch + assert parser.error == null + assert cmd.results != null + end + + fun test_cmd_parser_catalog_packages is test do + var parser = new CommandParser(test_view, test_builder, test_catalog) + var cmd = parser.parse("catalog:") + assert cmd isa CmdCatalogPackages + assert parser.error == null + assert cmd.results != null + end + + fun test_cmd_parser_catalog_stats is test do + var parser = new CommandParser(test_view, test_builder, test_catalog) + var cmd = parser.parse("stats:") + assert cmd isa CmdCatalogStats + assert parser.error == null + assert cmd.stats != null + end + + fun test_cmd_parser_catalog_tags is test do + var parser = new CommandParser(test_view, test_builder, test_catalog) + var cmd = parser.parse("tags:") + assert cmd isa CmdCatalogTags + assert parser.error == null + assert cmd.packages_count_by_tags != null + end + + fun test_cmd_parser_catalog_tag is test do + var parser = new CommandParser(test_view, test_builder, test_catalog) + var cmd = parser.parse("tag: test") + assert cmd isa CmdCatalogTag + assert parser.error == null + assert cmd.tag == "test" + assert cmd.results != null + end + + fun test_cmd_parser_catalog_person is test do + var parser = new CommandParser(test_view, test_builder, test_catalog) + var cmd = parser.parse("person: Alexandre Terrasa") + assert cmd isa CmdCatalogPerson + assert parser.error == null + assert cmd.person.as(not null).name == "Alexandre Terrasa" + end + + fun test_cmd_parser_catalog_contributing is test do + var parser = new CommandParser(test_view, test_builder, test_catalog) + var cmd = parser.parse("contrib: Alexandre Terrasa") + assert cmd isa CmdCatalogContributing + assert parser.error == null + assert cmd.person.as(not null).name == "Alexandre Terrasa" + assert cmd.results != null + end + + fun test_cmd_parser_catalog_maintaining is test do + var parser = new CommandParser(test_view, test_builder, test_catalog) + var cmd = parser.parse("maintain: Alexandre Terrasa") + assert cmd isa CmdCatalogMaintaining + assert parser.error == null + assert cmd.person.as(not null).name == "Alexandre Terrasa" + assert cmd.results != null + end +end