doc/commands: introduce commands parser
authorAlexandre Terrasa <alexandre@moz-code.org>
Tue, 24 Oct 2017 03:27:16 +0000 (23:27 -0400)
committerAlexandre Terrasa <alexandre@moz-code.org>
Thu, 23 Nov 2017 16:08:40 +0000 (11:08 -0500)
Signed-off-by: Alexandre Terrasa <alexandre@moz-code.org>

src/doc/commands/commands_parser.nit [new file with mode: 0644]
src/doc/commands/tests/test_commands_parser.nit [new file with mode: 0644]

diff --git a/src/doc/commands/commands_parser.nit b/src/doc/commands/commands_parser.nit
new file mode 100644 (file)
index 0000000..af75cbe
--- /dev/null
@@ -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 (file)
index 0000000..e58a157
--- /dev/null
@@ -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