From: Alexandre Terrasa Date: Tue, 24 Oct 2017 03:15:35 +0000 (-0400) Subject: doc/commands: introduce graph commands X-Git-Url: http://nitlanguage.org doc/commands: introduce graph commands Signed-off-by: Alexandre Terrasa --- diff --git a/src/doc/commands/commands_graph.nit b/src/doc/commands/commands_graph.nit new file mode 100644 index 0000000..7cb0f97 --- /dev/null +++ b/src/doc/commands/commands_graph.nit @@ -0,0 +1,392 @@ +# 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. + +# Graph commands +# +# Commands that return graphical representations about a Model or a MEntity. +module commands_graph + +import commands_model + +import uml +import dot + +# An abstract command that returns a dot graph +abstract class CmdGraph + super DocCommand + + # Rendering format + # + # Default is `dot`. + # See `allowed_formats`. + var format = "dot" is optional, writable + + # Allowed rendering formats. + # + # Can be `dot` or `svg`. + var allowed_formats: Array[String] = ["dot", "svg"] + + # Dot to render + var dot: nullable Writable = null is optional, writable + + # Render `dot` depending on `format` + fun render: nullable Writable do + var dot = self.dot + if dot == null then return null + if format == "svg" then + var proc = new ProcessDuplex("dot", "-Tsvg") + var svg = proc.write_and_read(dot.write_to_string) + proc.close + proc.wait + return svg + end + return dot + end + + redef fun init_command do + if not allowed_formats.has(format) then + return new ErrorBadGraphFormat(format, allowed_formats) + end + return super + end +end + +# Bad graph format requested +class ErrorBadGraphFormat + super CmdError + + # Provided format + var format: String + + # Allowed formats + var allowed_formats: Array[String] + + redef fun to_s do + var allowed_values = new Buffer + for allowed in allowed_formats do + allowed_values.append "`{allowed}`" + if allowed != allowed_formats.last then + allowed_values.append ", " + end + end + return "Bad format `{format}`. Allowed values are {allowed_values.write_to_string}." + end +end + +# UML command +# +# Return an UML diagram about a `mentity`. +class CmdUML + super CmdEntity + super CmdGraph + + autoinit(view, mentity, mentity_name, format, uml) + + # UML model to return + var uml: nullable UMLModel = null is optional, writable + + redef fun init_command do + if uml != null then return new CmdSuccess + + var res = super + if not res isa CmdSuccess then return res + var mentity = self.mentity.as(not null) + + if mentity isa MClassDef then mentity = mentity.mclass + if mentity isa MClass then + uml = new UMLModel(view, view.mainmodule) + else if mentity isa MModule then + uml = new UMLModel(view, view.mainmodule) + else + return new WarningNoUML(mentity) + end + return res + end + + redef fun render do + var uml = self.uml + if uml == null then return null + if mentity isa MClass then + dot = uml.generate_class_uml.write_to_string + else if mentity isa MModule then + dot = uml.generate_package_uml.write_to_string + end + return super + end +end + +# No UML model for `mentity` +class WarningNoUML + super CmdWarning + + # MEntity provided + var mentity: MEntity + + redef fun to_s do return "No UML for `{mentity.full_name}`" +end + +# Render a hierarchy graph for `mentity` if any. +class CmdInheritanceGraph + super CmdEntity + super CmdGraph + + autoinit(view, mentity, mentity_name, pdepth, cdepth, format, graph) + + # Parents depth to display + var pdepth: nullable Int = null is optional, writable + + # Children depth to display + var cdepth: nullable Int = null is optional, writable + + # Inheritance graph to return + var graph: nullable InheritanceGraph = null is optional, writable + + redef fun init_command do + if graph != null then return new CmdSuccess + + var res = super + if not res isa CmdSuccess then return res + var mentity = self.mentity.as(not null) + + graph = new InheritanceGraph(mentity, view) + return res + end + + redef fun render do + var graph = self.graph + if graph == null then return "" + self.dot = graph.draw(pdepth, cdepth).to_dot + return super + end +end + +# Graph for mentity hierarchies +# +# Recursively build parents and children list from a `center`. +class InheritanceGraph + + # MEntity at the center of this graph + var center: MEntity + + # ModelView used to filter graph + var view: ModelView + + # Graph generated + var graph: DotGraph is lazy do + var graph = new DotGraph("package_diagram", "digraph") + + graph["compound"] = "true" + graph["rankdir"] = "BT" + graph["ranksep"] = 0.3 + graph["nodesep"] = 0.3 + + graph.nodes_attrs["margin"] = 0.1 + graph.nodes_attrs["width"] = 0 + graph.nodes_attrs["height"] = 0 + graph.nodes_attrs["fontsize"] = 10 + graph.nodes_attrs["fontname"] = "helvetica" + + graph.edges_attrs["dir"] = "none" + graph.edges_attrs["color"] = "gray" + + return graph + end + + # Build the graph + fun draw(parents_depth, children_depth: nullable Int): DotGraph do + draw_node center + draw_parents(center, parents_depth) + draw_children(center, children_depth) + return graph + end + + private var nodes = new HashMap[MEntity, DotElement] + private var done_parents = new HashSet[MEntity] + private var done_children = new HashSet[MEntity] + + # Recursively draw parents of mentity + fun draw_parents(mentity: MEntity, max_depth: nullable Int, current_depth: nullable Int) do + if done_parents.has(mentity) then return + done_parents.add mentity + current_depth = current_depth or else 0 + if max_depth != null and current_depth >= max_depth then + from_dotdotdot(mentity) + return + end + var parents = mentity.collect_parents(view) + if parents.length > 10 then + from_dotdotdot(mentity) + return + end + for parent in parents do + if parent isa MModule then + var mgroup = parent.mgroup + if mgroup != null and mgroup.default_mmodule == parent then parent = mgroup + end + if parent isa MGroup then + if parent.mpackage.mgroups.first == parent then parent = parent.mpackage + end + draw_edge(mentity, parent) + end + for parent in parents do + if parent isa MModule then + var mgroup = parent.mgroup + if mgroup != null and mgroup.default_mmodule == parent then parent = mgroup + end + if parent isa MGroup then + if parent.mpackage.mgroups.first == parent then parent = parent.mpackage + end + draw_parents(parent, max_depth, current_depth + 1) + end + end + + # Recursively draw children of mentity + fun draw_children(mentity: MEntity, max_depth: nullable Int, current_depth: nullable Int) do + if done_children.has(mentity) then return + done_children.add mentity + current_depth = current_depth or else 0 + if max_depth != null and current_depth >= max_depth then + to_dotdotdot(mentity) + return + end + var children = mentity.collect_children(view) + if children.length > 10 then + to_dotdotdot(mentity) + return + end + for child in children do + if child isa MGroup then + if child.mpackage.mgroups.first == child then child = child.mpackage + end + draw_edge(child, mentity) + end + for child in children do + if child isa MGroup then + if child.mpackage.mgroups.first == child then child = child.mpackage + end + draw_children(child, max_depth, current_depth + 1) + end + end + + # Draw a node from a `mentity` + fun draw_node(mentity: MEntity): DotElement do + if nodes.has_key(mentity) then return nodes[mentity] + var node: DotElement = mentity.to_dot_node + if mentity == center then node = highlight(node) + nodes[mentity] = node + graph.add node + return node + end + + private var edges = new HashMap2[MEntity, MEntity, DotEdge] + + # Draw a edges between two mentities + fun draw_edge(from, to: MEntity): DotEdge do + if edges.has(from, to) then return edges[from, to].as(not null) + if edges.has(to, from) then return edges[to, from].as(not null) + var nfrom = draw_node(from) + var nto = draw_node(to) + var edge = new DotEdge(nfrom, nto) + edges[from, to] = edge + graph.add edge + return edge + end + + private var to_dots = new HashMap[MEntity, DotElement] + + # Create a link from `mentity` to a `...` node + fun to_dotdotdot(mentity: MEntity): DotEdge do + var nto = draw_node(mentity) + var dots = to_dots.get_or_null(mentity) + if dots == null then + dots = dotdotdot("{nto.id}...") + to_dots[mentity] = dots + end + graph.add dots + var edge = new DotEdge(dots, nto) + graph.add edge + return edge + end + + private var from_dots = new HashMap[MEntity, DotElement] + + # Create a link from a `...` node to a `mentity` + fun from_dotdotdot(mentity: MEntity): DotEdge do + var nfrom = draw_node(mentity) + var dots = to_dots.get_or_null(mentity) + if dots == null then + dots = dotdotdot("...{nfrom.id}") + from_dots[mentity] = dots + end + graph.add dots + var edge = new DotEdge(dots, nfrom) + graph.add edge + return edge + end + + # Change the border color of the node + fun highlight(dot: DotElement): DotElement do + dot["color"] = "#1E9431" + return dot + end + + # Generate a `...` node + fun dotdotdot(id: String): DotNode do + var node = new DotNode(id) + node["label"] = "..." + node["shape"] = "none" + return node + end +end + +redef class MEntity + # Return `self` as a DotNode + fun to_dot_node: DotNode do + var node = new DotNode(full_name) + node["label"] = name + return node + end +end + +redef class MPackage + redef fun to_dot_node do + var node = super + node["shape"] = "tab" + return node + end +end + +redef class MGroup + redef fun to_dot_node do + var node = super + node["shape"] = "folder" + return node + end +end + +redef class MModule + redef fun to_dot_node do + var node = super + node["shape"] = "note" + return node + end +end + +redef class MClass + redef fun to_dot_node do + var node = super + node["shape"] = "box" + return node + end +end diff --git a/src/doc/commands/tests/test_commands_graph.nit b/src/doc/commands/tests/test_commands_graph.nit new file mode 100644 index 0000000..04f5966 --- /dev/null +++ b/src/doc/commands/tests/test_commands_graph.nit @@ -0,0 +1,51 @@ +# 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_graph is test + +import test_commands +import doc::commands::commands_graph + +class TestCommandsGraph + super TestCommands + test + + fun test_cmd_uml is test do + var cmd = new CmdUML(test_view, mentity_name = "test_prog::Character") + var res = cmd.init_command + assert res isa CmdSuccess + assert cmd.uml != null + end + + fun test_cmd_uml_bad_format is test do + var cmd = new CmdUML(test_view, mentity_name = "test_prog::Character", format = "foo") + var res = cmd.init_command + assert res isa ErrorBadGraphFormat + assert cmd.uml == null + end + + fun test_cmd_uml_not_found is test do + var cmd = new CmdUML(test_view, mentity_name = "strength_bonus") + var res = cmd.init_command + assert res isa WarningNoUML + assert cmd.uml == null + end + + fun test_cmd_inh_graph is test do + var cmd = new CmdInheritanceGraph(test_view, mentity_name = "test_prog::Character") + var res = cmd.init_command + assert res isa CmdSuccess + assert cmd.graph != null + end +end