From d79626971fafc5595375a4dddcdf1388757c7ded Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Tue, 29 Aug 2017 18:22:15 -0400 Subject: [PATCH] src/doc_commands: rewrite doc commands parser Also add tests Signed-off-by: Alexandre Terrasa --- src/doc/doc_commands.nit | 260 ++++++++++++++++++++++++++++++----------- src/doc/test_doc_commands.nit | 143 +++++++++++++++++++++++ 2 files changed, 337 insertions(+), 66 deletions(-) create mode 100644 src/doc/test_doc_commands.nit diff --git a/src/doc/doc_commands.nit b/src/doc/doc_commands.nit index 1537704..e276057 100644 --- a/src/doc/doc_commands.nit +++ b/src/doc/doc_commands.nit @@ -19,129 +19,253 @@ # * `nitdoc` wikilinks like `[[doc: MEntity::name]]` module doc_commands -# A command aimed at a documentation tool like `nitdoc` or `nitx`. # -# `DocCommand` are generally of the form `command: args`. -interface DocCommand - - # Original command string. - fun string: String is abstract +class DocCommandParser - # Command name. - fun name: String is abstract + # List of allowed command names for this parser + var allowed_commands: Array[String] = [ "doc", "comment", "list", "param", + "return", "new", "call", "code", "graph"] is writable - # Command arguments. + # Parse `string` as a DocCommand # - # FIXME: define a syntax - fun args: Array[String] is abstract - - # Command factory. + # Returns `null` if the string cannot be parsed. + # + # ~~~ + # var parser = new DocCommandParser + # + # var command = parser.parse("comment: core::Array") + # assert command isa CommentCommand + # assert command.arg == "core::Array" # - # Returns a concrete instance of `DocCommand` depending on the string. - new(command_string: String) do - if command_string.has_prefix("doc:") then - return new ArticleCommand(command_string) - else if command_string.has_prefix("comment:") then - return new CommentCommand(command_string) - else if command_string.has_prefix("list:") then - return new ListCommand(command_string) - else if command_string.has_prefix("param:") then - return new ParamCommand(command_string) - else if command_string.has_prefix("return:") then - return new ReturnCommand(command_string) - else if command_string.has_prefix("new:") then - return new NewCommand(command_string) - else if command_string.has_prefix("call:") then - return new CallCommand(command_string) - else if command_string.has_prefix("code:") then - return new CodeCommand(command_string) - else if command_string.has_prefix("graph:") then - return new GraphCommand(command_string) + # command = parser.parse(":") # syntax error + # assert command == null + # assert parser.errors.not_empty + # ~~~ + fun parse(string: String): nullable DocCommand do + var pos = 0 + var tmp = new FlatBuffer + errors.clear + + # 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("empty command name", 0) + return null + end + if not allowed_commands.has(name) then + error("unknown command name", 0) + return null + end + + # Build the command + var command = new_command(name, string) + if command == null then + error("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 + if arg.is_empty then + error("empty command arg", pos) + return null + end + command.arg = arg + + # Parse command options + 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 + command.opts[oname] = oval + # TODO Check options end - return new UnknownCommand(command_string) + + 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: String): nullable DocCommand do + if name == "doc" then return new ArticleCommand(string) + if name == "comment" then return new CommentCommand(string) + if name == "list" then return new ListCommand(string) + if name == "param" then return new ParamCommand(string) + if name == "return" then return new ReturnCommand(string) + if name == "new" then return new NewCommand(string) + if name == "call" then return new CallCommand(string) + if name == "code" then return new CodeCommand(string) + if name == "graph" then return new GraphCommand(string) + return null + end + + # Errors and warnings from last call to `parse` + var errors = new Array[DocMessage] + + # Generate an error + fun error(message: String, col: nullable Int) do + errors.add new DocMessage(1, message, col) end - redef fun to_s do return string + # Generate a warning + fun warning(message: String, col: nullable Int) do + errors.add new DocMessage(2, message, col) + end end -# Used to factorize initialization of DocCommands. -abstract class AbstractDocCommand - super DocCommand +# A message generated by the DocCommandParser +class DocMessage + + # Message severity + # + # 1- Error + # 2- Warning + var level: Int - redef var string - redef var name is noinit - redef var args = new Array[String] + # Message explanatory string + var message: String - init do - # parse command + # Related column in original string if any + var col: nullable Int + + redef fun to_s do var str = new FlatBuffer - var i = 0 - while i < string.length do - var c = string[i] - i += 1 - if c == ':' then break - str.add c + if level == 1 then + str.append "Error: " + else + str.append "Warning: " + end + str.append message + var col = self.col + if col != null then + str.append " (col: {col})" + end + return str.write_to_string + end +end + +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 - name = str.write_to_string - # parse args - args.add string.substring_from(i).trim + return pos end end -# A `DocCommand` not recognized by documentation tools. +# A command aimed at a documentation tool like `nitdoc` or `nitx`. # -# Used to provide warnings or any other behavior for unexisting commands. -class UnknownCommand - super AbstractDocCommand +# `DocCommand` are generally of the form `command: arg | opt1: val1, opt2: val2`. +abstract class DocCommand + + # Original command string. + var string: String + + # Command name. + var name: String is noinit + + # Command arguments. + var arg: String is noinit, writable + + # Command options. + var opts = new HashMap[String, String] is writable + + redef fun to_s do + if opts.is_empty then + return "{name}: {arg}" + end + return "{name}: {arg} | {opts.join(", ", ": ")}" + end end # A `DocCommand` that includes the documentation article of a `MEntity`. # # Syntax: `doc: MEntity::name`. class ArticleCommand - super AbstractDocCommand + super DocCommand + + redef var name = "doc" end # A `DocCommand` that includes the MDoc of a `MEntity`. # # Syntax: `comment: MEntity::name`. class CommentCommand - super AbstractDocCommand + super DocCommand + + redef var name = "comment" end # A `DocCommand` that includes a list of something. # # Syntax: `list:kind: `. class ListCommand - super AbstractDocCommand + super DocCommand + + redef var name = "list" end # A `DocCommand` that includes the list of methods tanking a `MType` as parameter. # # Syntax: `param: MType`. class ParamCommand - super AbstractDocCommand + super DocCommand + + redef var name = "param" end # A `DocCommand` that includes the list of methods returning a `MType` as parameter. # -# Syntax: `param: MType`. +# Syntax: `return: MType`. class ReturnCommand - super AbstractDocCommand + super DocCommand + + redef var name = "return" end # A `DocCommand` that includes the list of methods creating new instances of a specific `MType` # # Syntax: `new: MType`. class NewCommand - super AbstractDocCommand + super DocCommand + + redef var name = "new" end # A `DocCommand` that includes the list of methods calling a specific `MProperty`. # # Syntax: `call: MEntity::name`. class CallCommand - super AbstractDocCommand + super DocCommand + + redef var name = "call" end # A `DocCommand` that includes the source code of a `MEntity`. @@ -151,7 +275,9 @@ end # * `./src/file.nit` to include source code from a file. # * `./src/file.nit:1,2--3,4` to select code between positions. class CodeCommand - super AbstractDocCommand + super DocCommand + + redef var name = "code" end # A `DocCommand` that display an graph for a `MEntity`. @@ -159,5 +285,7 @@ end # Syntax: # * `graph: MEntity::name` class GraphCommand - super AbstractDocCommand + super DocCommand + + redef var name = "graph" end diff --git a/src/doc/test_doc_commands.nit b/src/doc/test_doc_commands.nit new file mode 100644 index 0000000..712d4cd --- /dev/null +++ b/src/doc/test_doc_commands.nit @@ -0,0 +1,143 @@ +# 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_doc_commands is test + +import doc_commands + +class TestDocCommandParser + test + + var parser: DocCommandParser + + fun init_parser is before do + parser = new DocCommandParser + end + + fun test_empty_string is test do + var command = parser.parse("") + assert command == null + assert parser.errors.length == 1 + assert parser.errors.first.to_s == "Error: empty command name (col: 0)" + end + + fun test_bad_string is test do + var command = parser.parse(":") + assert command == null + assert parser.errors.length == 1 + assert parser.errors.first.to_s == "Error: empty command name (col: 0)" + end + + fun test_unknown_command is test do + var command = parser.parse("foo: foo") + assert command == null + assert parser.errors.length == 1 + assert parser.errors.first.to_s == "Error: unknown command name (col: 0)" + end + + fun test_unallowed_command is test do + parser.allowed_commands.clear + var command = parser.parse("comment: core::Array") + assert command == null + assert parser.errors.length == 1 + assert parser.errors.first.to_s == "Error: unknown command name (col: 0)" + end + + fun test_no_arg is test do + var command = parser.parse("doc:") + assert command == null + assert parser.errors.length == 1 + print parser.errors.first + assert parser.errors.first.to_s == "Error: empty command arg (col: 4)" + end + + fun test_no_opts is test do + var command = parser.parse("doc: core::Array") + assert command isa ArticleCommand + assert command.name == "doc" + assert command.arg == "core::Array" + assert parser.errors.is_empty + end + + fun test_opts_empty is test do + var command = parser.parse("doc: core::Array | ") + assert command isa ArticleCommand + assert command.name == "doc" + assert command.arg == "core::Array" + assert parser.errors.is_empty + end + + fun test_1_opt is test do + var command = parser.parse("doc: core::Array | opt1: val1 ") + assert command isa ArticleCommand + assert command.name == "doc" + assert command.arg == "core::Array" + assert command.opts.length == 1 + assert command.opts["opt1"] == "val1" + assert parser.errors.is_empty + end + + fun test_2_opts is test do + var command = parser.parse("doc: core::Array | opt1: val1 , opt2: val2, ") + assert command isa ArticleCommand + assert command.name == "doc" + assert command.arg == "core::Array" + assert command.opts.length == 2 + assert command.opts["opt1"] == "val1" + assert command.opts["opt2"] == "val2" + assert parser.errors.is_empty + end + + fun test_empty_opt_name is test do + var command = parser.parse("doc: core::Array | opt1: val1 , :") + assert command isa ArticleCommand + assert command.name == "doc" + assert command.arg == "core::Array" + assert command.opts.length == 1 + assert command.opts["opt1"] == "val1" + assert parser.errors.is_empty + end + + fun test_empty_opt_value is test do + var command = parser.parse("doc: core::Array | opt1: , opt2: val2, ") + assert command isa ArticleCommand + assert command.name == "doc" + assert command.arg == "core::Array" + assert command.opts.length == 2 + assert command.opts["opt1"] == "" + assert command.opts["opt2"] == "val2" + assert parser.errors.is_empty + end + + fun test_empty_opt_value2 is test do + var command = parser.parse("doc: core::Array | opt1") + assert command isa ArticleCommand + assert command.name == "doc" + assert command.arg == "core::Array" + assert command.opts.length == 1 + assert command.opts["opt1"] == "" + assert parser.errors.is_empty + end + + fun test_empty_opt_value3 is test do + var command = parser.parse("doc: core::Array | opt1, opt2: val2") + assert command isa ArticleCommand + assert command.name == "doc" + assert command.arg == "core::Array" + assert command.opts.length == 2 + assert command.opts["opt1"] == "" + assert command.opts["opt2"] == "val2" + assert parser.errors.is_empty + end +end -- 1.7.9.5