neo_doxygen: Introduce a tool to import a Doxygen model.
authorJean-Christophe Beaupré <jcbrinfo@users.noreply.github.com>
Tue, 4 Nov 2014 17:13:46 +0000 (12:13 -0500)
committerJean-Christophe Beaupré <jcbrinfo@users.noreply.github.com>
Tue, 4 Nov 2014 17:13:46 +0000 (12:13 -0500)
Import the following:

* Classes
* Namespaces
* Inheritance relationships
* Documentation of classes (not formatted yet)

Signed-off-by: Jean-Christophe Beaupré <jcbrinfo@users.noreply.github.com>

12 files changed:
contrib/neo_doxygen/Makefile [new file with mode: 0644]
contrib/neo_doxygen/src/doxml/compounddef.nit [new file with mode: 0644]
contrib/neo_doxygen/src/doxml/doc.nit [new file with mode: 0644]
contrib/neo_doxygen/src/doxml/doxml.nit [new file with mode: 0644]
contrib/neo_doxygen/src/doxml/entitydef.nit [new file with mode: 0644]
contrib/neo_doxygen/src/doxml/listener.nit [new file with mode: 0644]
contrib/neo_doxygen/src/model/class_compound.nit [new file with mode: 0644]
contrib/neo_doxygen/src/model/graph.nit [new file with mode: 0644]
contrib/neo_doxygen/src/model/location.nit [new file with mode: 0644]
contrib/neo_doxygen/src/model/model.nit [new file with mode: 0644]
contrib/neo_doxygen/src/model/module_compound.nit [new file with mode: 0644]
contrib/neo_doxygen/src/neo_doxygen.nit [new file with mode: 0644]

diff --git a/contrib/neo_doxygen/Makefile b/contrib/neo_doxygen/Makefile
new file mode 100644 (file)
index 0000000..ed82923
--- /dev/null
@@ -0,0 +1,30 @@
+# 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.
+
+NITG=../../bin/nitg
+NITG_FLAGS=--dir bin
+NEO4J_DIR=/var/lib/neo4j
+
+.PHONY: bin reset-neo
+
+# Compile the tool.
+bin:
+       mkdir -p bin
+       ../../bin/nitg --dir bin src/neo_doxygen.nit
+
+# Reset the local graph.
+reset-neo:
+       sudo -u neo4j "${NEO4J_DIR}/bin/neo4j" stop \
+       && sudo -u neo4j rm -rf "${NEO4J_DIR}/data/graph.db" \
+       && sudo -u neo4j "${NEO4J_DIR}/bin/neo4j" start
diff --git a/contrib/neo_doxygen/src/doxml/compounddef.nit b/contrib/neo_doxygen/src/doxml/compounddef.nit
new file mode 100644 (file)
index 0000000..b95ee14
--- /dev/null
@@ -0,0 +1,71 @@
+# 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.
+
+# `compounddef` element reading.
+module doxml::compounddef
+
+import entitydef
+
+# Processes the content of a `compounddef` element.
+class CompoundDefListener
+       super EntityDefListener
+
+       var compound: Compound is writable, noinit
+
+
+       # Attributes of the current `<basecompoundref>` element.
+
+       private var refid = ""
+       private var prot = ""
+       private var virt = ""
+
+
+       init do
+               super
+       end
+
+       redef fun entity: Entity do return compound
+
+       redef fun start_dox_element(local_name: String, atts: Attributes) do
+               if ["compoundname", "innerclass", "innernamespace"].has(local_name) then
+                       text.listen_until(dox_uri, local_name)
+                       if ["innerclass", "innernamespace"].has(local_name) then
+                               refid = get_required(atts, "refid")
+                       end
+               else if "basecompoundref" == local_name then
+                       refid = get_optional(atts, "refid", "")
+                       prot = get_optional(atts, "prot", "")
+                       virt = get_optional(atts, "virt", "")
+                       text.listen_until(dox_uri, local_name)
+               else
+                       super
+               end
+       end
+
+       redef fun end_dox_element(local_name: String) do
+               if local_name == "compounddef" then
+                       compound.put_in_graph
+               else if local_name == "compoundname" then
+                       compound.full_name = text.to_s
+               else if local_name == "innerclass" then
+                       compound.declare_class(refid, text.to_s)
+               else if local_name == "innernamespace" then
+                       compound.declare_namespace(refid, text.to_s)
+               else if local_name == "basecompoundref" then
+                       compound.declare_super(refid, text.to_s, prot, virt)
+               else
+                       super
+               end
+       end
+end
diff --git a/contrib/neo_doxygen/src/doxml/doc.nit b/contrib/neo_doxygen/src/doxml/doc.nit
new file mode 100644 (file)
index 0000000..84a880d
--- /dev/null
@@ -0,0 +1,30 @@
+# 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.
+
+# Documentation reading.
+module doxml::doc
+
+import listener
+
+# Processes documentation.
+class DocListener
+       super TextListener
+
+       var doc: JsonArray = new JsonArray is writable
+
+       redef fun end_listening do
+               super
+               doc.add(to_s)
+       end
+end
diff --git a/contrib/neo_doxygen/src/doxml/doxml.nit b/contrib/neo_doxygen/src/doxml/doxml.nit
new file mode 100644 (file)
index 0000000..431d514
--- /dev/null
@@ -0,0 +1,86 @@
+# 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.
+
+# Doxygen’s XML documents reading.
+module doxml
+
+import compounddef
+
+# Reader for XML documents whose the schema is `compound.xsd`.
+class CompoundFileReader
+       super DoxmlListener
+
+       # The project graph.
+       var model: ProjectGraph
+
+       private var reader: XMLReader = new XophonReader
+       private var compounddef: CompoundDefListener is noinit
+       private var noop: NoopListener is noinit
+
+       init do
+               compounddef = new CompoundDefListener(reader, self)
+               noop = new NoopListener(reader, self)
+       end
+
+       redef fun graph do return model
+
+       # Read the document at the specified path.
+       fun read(path: String) do
+               reader.content_handler = self
+               reader.parse_file(path)
+               compounddef.compound = new UnknownCompound(model)
+       end
+
+       redef fun start_dox_element(local_name: String, atts: Attributes) do
+               if local_name == "compounddef" then
+                       read_compound(atts)
+               else if "doxygen" != local_name then
+                       noop.listen_until(dox_uri, local_name)
+               end
+       end
+
+       private fun read_compound(atts: Attributes) do
+               var kind = get_required(atts, "kind")
+
+               create_compound(kind)
+               # TODO Make all values of `kind` and `visibility` compatible with the Nit meta-model.
+               if get_bool(atts, "final") then
+                       kind = "final {kind}"
+               end
+               if get_bool(atts, "sealed") then
+                       kind = "sealed {kind}"
+               end
+               if get_bool(atts, "abstract") then
+                       kind = "abstract {kind}"
+               end
+               compounddef.compound.kind = kind
+               compounddef.compound.model_id = get_required(atts, "id")
+               compounddef.compound.visibility = get_optional(atts, "prot", "")
+       end
+
+       private fun create_compound(kind: String) do
+               if kind == "file" then
+                       compounddef.compound = new FileCompound(model)
+               else if kind == "namespace" then
+                       compounddef.compound = new Namespace(model)
+               else if kind == "class" or kind == "interface" or kind == "enum" then
+                       compounddef.compound = new ClassCompound(model)
+               else
+                       compounddef.compound = new UnknownCompound(model)
+                       noop.listen_until(dox_uri, "compounddef")
+                       return
+               end
+               compounddef.listen_until(dox_uri, "compounddef")
+       end
+end
diff --git a/contrib/neo_doxygen/src/doxml/entitydef.nit b/contrib/neo_doxygen/src/doxml/entitydef.nit
new file mode 100644 (file)
index 0000000..fd9ae68
--- /dev/null
@@ -0,0 +1,77 @@
+# 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.
+
+# Common SAX listeners for entity definitions.
+module doxml::entitydef
+
+import doc
+
+# Processes the content of an entity definition.
+abstract class EntityDefListener
+       super StackableListener
+
+       # The inner `TextListener`.
+       protected var text: TextListener is noinit
+
+       # The inner `DocListener`.
+       protected var doc: DocListener is noinit
+
+       # The inner `NoopListener`.
+       protected var noop: NoopListener is noinit
+
+       init do
+               super
+               text = new TextListener(reader, self)
+               doc = new DocListener(reader, self)
+               noop = new NoopListener(reader, self)
+       end
+
+       # The current entity.
+       protected fun entity: Entity is abstract
+
+       redef fun start_dox_element(local_name: String, atts: Attributes) do
+               if ["briefdescription", "detaileddescription", "inbodydescription"].has(local_name) then
+                       doc.doc = entity.doc
+                       doc.listen_until(dox_uri, local_name)
+               else if "location" == local_name then
+                       entity.location = get_location(atts)
+               else
+                       noop.listen_until(dox_uri, local_name)
+               end
+       end
+
+       redef fun end_listening do
+               super
+               entity.put_in_graph
+       end
+
+       # Parse the attributes of a `location` element.
+       protected fun get_location(atts: Attributes): Location do
+               var location = new Location
+
+               location.path = atts.value_ns("", "bodyfile") or else atts.value_ns("", "file")
+               # Doxygen may indicate `[generated]`.
+               if "[generated]" == location.path then location.path = null
+               var line_start = atts.value_ns("", "bodystart") or else atts.value_ns("", "line") or else null
+               if line_start != null then location.line_start = line_start.to_i
+               var line_end = atts.value_ns("", "bodyend")
+               if line_end != null then location.line_end = line_end.to_i
+               var column_start = atts.value_ns("", "column")
+               if column_start != null then location.column_start = column_start.to_i
+               if location.line_start == location.line_end then
+                       location.column_end = location.column_start
+               end
+               return location
+       end
+end
diff --git a/contrib/neo_doxygen/src/doxml/listener.nit b/contrib/neo_doxygen/src/doxml/listener.nit
new file mode 100644 (file)
index 0000000..3c4dfa8
--- /dev/null
@@ -0,0 +1,222 @@
+# 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.
+
+# Basic SAX listeners.
+module doxml::listener
+
+import saxophonit
+import model
+
+# Common abstractions for SAX listeners reading XML documents generated by Doxygen.
+abstract class DoxmlListener
+       super ContentHandler
+
+       # The locator setted by calling `document_locator=`.
+       protected var locator: nullable SAXLocator = null
+
+       # The project graph.
+       fun graph: ProjectGraph is abstract
+
+       redef fun document_locator=(locator: SAXLocator) do
+               self.locator = locator
+       end
+
+       protected fun dox_uri: String do return ""
+
+       redef fun start_element(uri: String, local_name: String, qname: String,
+                       atts: Attributes) do
+               super
+               if uri != dox_uri then return # None of our business.
+               start_dox_element(local_name, atts)
+       end
+
+       # Process the start of an element in the Doxygen’s namespace.
+       #
+       # See `ContentHandler.start_element` for the description of the parameters.
+       protected fun start_dox_element(local_name: String, atts: Attributes) do end
+
+       redef fun end_element(uri: String, local_name: String, qname: String) do
+               super
+               if uri != dox_uri then return # None of our business.
+               end_dox_element(local_name)
+       end
+
+       # Process the end of an element in the Doxygen’s namespace.
+       #
+       # See `ContentHandler.start_element` for the description of the parameters.
+       protected fun end_dox_element(local_name: String) do end
+
+       protected fun get_bool(atts: Attributes, local_name: String): Bool do
+               return get_optional(atts, local_name, "no") == "yes"
+       end
+
+       # Get the value of an optional attribute.
+       #
+       # Parameters:
+       #
+       # * `atts`: attribute list.
+       # * `local_name`: local name of the attribute.
+       # * `default`: value to return when the specified attribute is not found.
+       protected fun get_optional(atts: Attributes, local_name: String,
+                       default: String): String do
+               return atts.value_ns(dox_uri, local_name) or else default
+       end
+
+       # Get the value of an required attribute.
+       #
+       # Parameters:
+       #
+       # * `atts`: attribute list.
+       # * `local_name`: local name of the attribute.
+       protected fun get_required(atts: Attributes, local_name: String): String do
+               var value = atts.value_ns(dox_uri, local_name)
+               if value == null then
+                       throw_error("The `{local_name}` attribute is required.")
+                       return ""
+               else
+                       return value
+               end
+       end
+
+       redef fun end_document do
+               locator = null
+       end
+
+       # Throw an error with the specified message by prepending the current location.
+       protected fun throw_error(message: String) do
+               var e: SAXParseException
+
+               if locator != null then
+                       e = new SAXParseException.with_locator(message, locator.as(not null))
+               else
+                       e = new SAXParseException(message)
+               end
+               e.throw
+       end
+end
+
+# A `DoxmlListener` that read only a part of a document.
+#
+# Temporary redirect events to itself until it ends processing its part.
+abstract class StackableListener
+       super DoxmlListener
+
+       # The associated reader.
+       var reader: XMLReader
+
+       # The parent listener.
+       var parent: DoxmlListener
+
+       # Namespace’s IRI of the element at the root of the part to process.
+       private var root_uri: String = ""
+
+       # Local name of the element at the root of the part to process.
+       private var root_local_name: String = ""
+
+       # The number of open element of the same type than the root of the part to process.
+       private var depth = 0
+
+       # The project graph.
+       private var p_graph: ProjectGraph is noinit
+
+
+       init do
+               super
+               p_graph = parent.graph
+       end
+
+       redef fun graph do return p_graph
+
+       # Temporary redirect events to itself until the end of the specified element.
+       fun listen_until(uri: String, local_name: String) do
+               root_uri = uri
+               root_local_name = local_name
+               depth = 1
+               reader.content_handler = self
+               locator = parent.locator
+       end
+
+       redef fun start_element(uri: String, local_name: String, qname: String,
+                       atts: Attributes) do
+               super
+               if uri == root_uri and local_name == root_local_name then
+                       depth += 1
+               end
+       end
+
+       redef fun end_element(uri: String, local_name: String, qname: String) do
+               super
+               if uri == root_uri and local_name == root_local_name then
+                       depth -= 1
+                       if depth <= 0 then
+                               end_listening
+                               parent.end_element(uri, local_name, qname)
+                       end
+               end
+       end
+
+       # Reset the reader’s listener to the parent.
+       fun end_listening do
+               reader.content_handler = parent
+               locator = null
+       end
+
+       redef fun end_document do
+               end_listening
+       end
+end
+
+# A SAX listener that skips any event except the end of the part to process.
+#
+# Used to skip an entire element.
+class NoopListener
+       super StackableListener
+end
+
+# Concatenates any text node found.
+class TextListener
+       super StackableListener
+
+       protected var buffer: Buffer = new FlatBuffer
+       private var sp: Bool = false
+
+       redef fun listen_until(uri: String, local_name: String) do
+               buffer.clear
+               sp = false
+               super
+       end
+
+       redef fun characters(str: String) do
+               if sp then
+                       if buffer.length > 0 then buffer.append(" ")
+                       sp = false
+               end
+               buffer.append(str)
+       end
+
+       redef fun ignorable_whitespace(str: String) do
+               sp = true
+       end
+
+       # Flush the buffer.
+       protected fun flush_buffer: String do
+               var s = buffer.to_s
+
+               buffer.clear
+               sp = false
+               return s
+       end
+
+       redef fun to_s do return buffer.to_s
+end
diff --git a/contrib/neo_doxygen/src/model/class_compound.nit b/contrib/neo_doxygen/src/model/class_compound.nit
new file mode 100644 (file)
index 0000000..005d25f
--- /dev/null
@@ -0,0 +1,118 @@
+# 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.
+
+# Nodes for classes.
+module model::class_compound
+
+import graph
+
+# A class.
+class ClassCompound
+       super Compound
+
+       # The corresponding type.
+       #
+       # In the case of a generic class, defines bounds for type parameters.
+       var class_type: ClassType is noinit
+
+       # The definition.
+       var class_def: ClassDef is noinit
+
+       init do
+               super
+               class_type = new ClassType(graph, self)
+               class_def = new ClassDef(graph, self)
+               self.labels.add("MClass")
+               self["arity"] = 0 # TODO
+               kind = "class"
+               visibility = "public"
+       end
+
+       redef fun name=(name: String) do
+               super
+               class_type.name = name
+               class_def.name = name
+       end
+
+       redef fun location=(location: nullable Location) do
+               super
+               class_def.location = location
+       end
+
+       redef fun set_mdoc do
+               super
+               class_def["mdoc"] = doc
+       end
+
+       redef fun declare_super(id: String, name: String, prot: String, virt: String) do
+               class_def.declare_super(id, name, prot, virt)
+       end
+
+       redef fun put_in_graph do
+               super
+               class_type.put_in_graph
+               class_def.put_in_graph
+       end
+
+       redef fun put_edges do
+               super
+               graph.add_edge(self, "CLASSTYPE", class_type)
+       end
+end
+
+# The `MClassDef` node of a class.
+class ClassDef
+       super CodeBlock
+
+       var class_compound: ClassCompound
+       var supers: SimpleCollection[String] = new Array[String]
+
+       init do
+               super
+               self.labels.add("MClassDef")
+       end
+
+       fun declare_super(id: String, name: String, prot: String, virt: String) do
+               # TODO prot, virt, name
+               if "" != id then
+                       supers.add(id)
+               end
+       end
+
+       redef fun put_edges do
+               super
+               graph.add_edge(self, "BOUNDTYPE", class_compound.class_type)
+               graph.add_edge(self, "MCLASS", class_compound)
+               for s in supers do
+                       graph.add_edge(self, "INHERITS", graph.by_id[s].as(ClassCompound).class_type)
+               end
+       end
+end
+
+# A type defined by a class.
+class ClassType
+       super Entity
+
+       var class_compound: ClassCompound
+
+       init do
+               super
+               self.labels.add("MType")
+               self.labels.add("MClassType")
+       end
+
+       redef fun put_edges do
+               graph.add_edge(self, "CLASS", class_compound)
+       end
+end
diff --git a/contrib/neo_doxygen/src/model/graph.nit b/contrib/neo_doxygen/src/model/graph.nit
new file mode 100644 (file)
index 0000000..e14d817
--- /dev/null
@@ -0,0 +1,293 @@
+# 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.
+
+# Graphs and basic entities.
+module model::graph
+
+import neo4j
+import location
+
+# A Neo4j graph.
+class NeoGraph
+       var all_nodes: SimpleCollection[NeoNode] = new Array[NeoNode]
+       var all_edges: SimpleCollection[NeoEdge] = new Array[NeoEdge]
+
+       # Add a relationship between two nodes.
+       #
+       # Parameters are the same than for the constructor of `NeoEdge`.
+       fun add_edge(from: NeoNode, rel_type: String, to: NeoNode) do
+               all_edges.add(new NeoEdge(from, rel_type, to))
+       end
+end
+
+# The project’s graph.
+class ProjectGraph
+       super NeoGraph
+
+       # The node reperesenting the project.
+       #
+       # Once the project’s graph is initialized, this node must not be edited.
+       var project: NeoNode = new NeoNode
+
+       # Entities by `model_id`.
+       var by_id: Map[String, Entity] = new HashMap[String, Entity]
+
+       # Initialize a new project graph using the specified project name.
+       #
+       # The specified name will label all nodes of the project’s graph.
+       init(name: String) do
+               project.labels.add(name)
+               project.labels.add("MEntity")
+               project.labels.add("MProject")
+               project["name"] = name
+               all_nodes.add(project)
+
+               var root = new RootNamespace(self)
+               root.put_in_graph
+               by_id[""] = root
+       end
+
+       # Request to all nodes in the graph to add their related edges.
+       fun put_edges do
+               add_edge(project, "ROOT", by_id[""])
+               for n in all_nodes do
+                       if n isa Entity then
+                               n.put_edges
+                       end
+               end
+       end
+end
+
+# A model’s entity.
+#
+# In practice, this is the base class of every node in a `ProjectGraph`.
+abstract class Entity
+       super NeoNode
+
+       # Graph that will embed the entity.
+       var graph: ProjectGraph
+
+       # ID of the entity in the model.
+       #
+       # Is empty for entities without an ID.
+       var model_id: String = "" is writable
+
+       # Associated documentation.
+       var doc: JsonArray = new JsonArray is writable
+
+       init do
+               self.labels.add(graph.project["name"].to_s)
+               self.labels.add("MEntity")
+       end
+
+       # The short (unqualified) name.
+       #
+       # May be also set by `full_name=`.
+       fun name=(name: String) do
+               self["name"] = name
+       end
+
+       # The short (unqualified) name.
+       fun name: String do
+               var name = self["name"]
+               assert name isa String
+               return name
+       end
+
+       # Include the documentation of `self` in the graph.
+       protected fun set_mdoc do
+               self["mdoc"] = doc
+       end
+
+       # The namespace separator of Nit/C++.
+       fun ns_separator: String do return "::"
+
+       # The name separator used when calling `full_name=`.
+       fun name_separator: String do return ns_separator
+
+       # The full (qualified) name.
+       #
+       # Also set `name` using `name_separator`.
+       fun full_name=(full_name: String) do
+               var m: nullable Match = full_name.search_last(name_separator)
+
+               self["full_name"] = full_name
+               if m == null then
+                       name = full_name
+               else
+                       name = full_name.substring_from(m.after)
+               end
+       end
+
+       # The full (qualified) name.
+       fun full_name: String do
+               var full_name = self["full_name"]
+               assert full_name isa String
+               return full_name
+       end
+
+       # Set the full name using the current name and the specified parent name.
+       fun parent_name=(parent_name: String) do
+               self["full_name"] = parent_name + name_separator + self["name"].as(not null).to_s
+       end
+
+       # Set the location of the entity in the source code.
+       fun location=(location: nullable Location) do
+               self["location"] = location
+       end
+
+       # Put the entity in the graph.
+       #
+       # Called by the loader when it has finished to read the entity.
+       fun put_in_graph do
+               if doc.length > 0 then
+                       set_mdoc
+               end
+               graph.all_nodes.add(self)
+               if model_id != "" then graph.by_id[model_id] = self
+       end
+
+       # Put the related edges in the graph.
+       #
+       # This method is called on each node by `ProjectGraph.put_edges`.
+       #
+       # Note: Even at this step, the entity may modify its own attributes and
+       # inner entities’ ones because some values are only known once the entity
+       # know its relationships with the rest of the graph.
+       fun put_edges do end
+end
+
+# An entity whose the location is mandatory.
+abstract class CodeBlock
+       super Entity
+
+       init do
+               self["location"] = new Location
+       end
+
+       redef fun location=(location: nullable Location) do
+               if location == null then
+                       super(new Location)
+               else
+                       super
+               end
+       end
+end
+
+# A compound.
+#
+# Usually corresponds to a `<compounddef>` element in of the XML output of
+# Doxygen.
+abstract class Compound
+       super Entity
+
+       # Set the declared visibility (the proctection) of the compound.
+       fun visibility=(visibility: String) do
+               self["visibility"] = visibility
+       end
+
+       # Set the specific kind of the compound.
+       fun kind=(kind: String) do
+               self["kind"] = kind
+       end
+
+       # Declare an inner namespace.
+       #
+       # Parameters:
+       #
+       # * `id`: `model_id` of the inner namespace. May be empty.
+       # * `name`: string identifying the inner namespace. May be empty.
+       fun declare_namespace(id: String, name: String) do end
+
+       # Declare an inner class.
+       #
+       # Parameters:
+       #
+       # * `id`: `model_id` of the inner class. May be empty.
+       # * `name`: string identifying the inner class. May be empty.
+       fun declare_class(id: String, name: String) do end
+
+       # Declare a base compound (usually, a base class).
+       #
+       # Parameters:
+       #
+       # * `id`: `model_id` of the base compound. May be empty.
+       # * `name`: string identifying the base compound. May be empty.
+       # * `prot`: visibility (proctection) of the relationship.
+       # * `virt`: level of virtuality of the relationship.
+       fun declare_super(id: String, name: String, prot: String, virt: String) do end
+end
+
+# An unrecognized compound.
+#
+# Used to simplify the handling of ignored entities.
+class UnknownCompound
+       super Compound
+
+       redef fun put_in_graph do end
+       redef fun put_edges do end
+end
+
+# A namespace.
+#
+# Corresponds to a group in Nit.
+class Namespace
+       super Compound
+
+       # Inner namespaces (IDs).
+       #
+       # Left empty for the root namespace.
+       var inner_namespaces: SimpleCollection[String] = new Array[String]
+
+       init do
+               super
+               self.labels.add("MGroup")
+       end
+
+       redef fun declare_namespace(id: String, name: String) do
+               inner_namespaces.add(id)
+       end
+
+       redef fun put_edges do
+               super
+               graph.add_edge(self, "PROJECT", graph.project)
+               if self["name"] == self["full_name"] and self["full_name"] != "" then
+                       # The root namespace does not know its children.
+                       var root = graph.by_id[""]
+                       graph.add_edge(self, "PARENT", root)
+                       graph.add_edge(root, "NESTS", self)
+               end
+               for ns in inner_namespaces do
+                       var node = graph.by_id[ns]
+                       graph.add_edge(node, "PARENT", self)
+                       graph.add_edge(self, "NESTS", node)
+               end
+       end
+end
+
+# The root namespace of a `ProjectGraph`.
+#
+# This the only entity in the graph whose `model_id` is really `""`.
+# Added automatically at the initialization of a `ProjectGraph`.
+class RootNamespace
+       super Namespace
+
+       init do
+               super
+               self["full_name"] = ""
+               self["name"] = graph.project["name"]
+       end
+
+       redef fun declare_namespace(id: String, name: String) do end
+end
diff --git a/contrib/neo_doxygen/src/model/location.nit b/contrib/neo_doxygen/src/model/location.nit
new file mode 100644 (file)
index 0000000..8652577
--- /dev/null
@@ -0,0 +1,37 @@
+# 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.
+
+# This module is used to model locations in source files.
+module location
+
+import neo4j
+
+# A location inside a source file.
+class Location
+       super Jsonable
+
+       var path: nullable String = null is writable
+       var line_start: Int = 1 is writable
+       var line_end: Int = 1 is writable
+       var column_start: Int = 1 is writable
+       var column_end: Int = 1 is writable
+
+       redef fun to_s: String do
+               var file_part = "/dev/null:"
+               if path != null and path.length > 0 then file_part = "{path.as(not null)}:"
+               return "{file_part}{line_start},{column_start}--{line_end},{column_end}"
+       end
+
+       redef fun to_json do return to_s.to_json
+end
diff --git a/contrib/neo_doxygen/src/model/model.nit b/contrib/neo_doxygen/src/model/model.nit
new file mode 100644 (file)
index 0000000..443259e
--- /dev/null
@@ -0,0 +1,21 @@
+# 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.
+
+# The model used to populate the Neo4j graph.
+module model
+
+import location
+import graph
+import class_compound
+import module_compound
diff --git a/contrib/neo_doxygen/src/model/module_compound.nit b/contrib/neo_doxygen/src/model/module_compound.nit
new file mode 100644 (file)
index 0000000..eb1f297
--- /dev/null
@@ -0,0 +1,139 @@
+# 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.
+
+# Nodes for modules and files.
+module model::module_compound
+
+import graph
+import class_compound
+
+# A source file.
+#
+# Creates one modules by inner namespace. The full name of the modules begin
+# with the namespace’s full name, and end with the unqualified name of the file,
+# without the extension.
+class FileCompound
+       super Compound
+       super CodeBlock
+
+       # Mapping between inner namespace’s names and corresponding modules.
+       private var inner_namespaces: Map[String, Module] = new HashMap[String, Module]
+
+       # The last component of the path, without the extension.
+       #
+       # Used as the unqualified name of the modules.
+       private var basename: String = ""
+
+       init do
+               super
+       end
+
+       redef fun name_separator: String do return "/"
+
+       redef fun location=(location: nullable Location) do
+               super
+               if location != null and location.path != null then
+                       full_name = location.path.as(not null)
+               end
+               for m in inner_namespaces.values do m.location = location
+       end
+
+       redef fun name=(name: String) do
+               # Example: `MyClass.java`
+               super
+               var match = name.search_last(".")
+
+               if match == null then
+                       basename = name
+               else
+                       basename = name.substring(0, match.from)
+               end
+               # Update the modules’ name.
+               for ns, m in inner_namespaces do
+                       m.full_name = "{ns}{ns_separator}{basename}"
+               end
+       end
+
+       redef fun declare_namespace(id: String, name: String) do
+               var m: Module
+
+               if inner_namespaces.keys.has(name) then
+                       m = inner_namespaces[name]
+                       m.parent = id
+               else
+                       m = new Module(graph)
+                       m.full_name = "{name}{ns_separator}{basename}"
+                       m.parent = id
+                       m.location = self["location"].as(nullable Location)
+                       inner_namespaces[name] = m
+               end
+       end
+
+       redef fun declare_class(id: String, name: String) do
+               var match = name.search_last(ns_separator)
+               var ns_name: String
+               var m: Module
+
+               if match == null then
+                       ns_name = ""
+               else
+                       ns_name = name.substring(0, match.from)
+               end
+               if inner_namespaces.keys.has(ns_name) then
+                       m = inner_namespaces[ns_name]
+               else
+                       declare_namespace("", ns_name)
+                       m = inner_namespaces[ns_name]
+               end
+               m.declare_class(id, name)
+       end
+
+       redef fun put_in_graph do
+               # Do not add `self` to the Neo4j graph...
+               # ... but add its modules...
+               for m in inner_namespaces.values do m.put_in_graph
+               # ... and add `self` to the index.
+               if model_id != "" then graph.by_id[model_id] = self
+       end
+end
+
+# A module.
+class Module
+       super Compound
+       super CodeBlock
+
+       # The `model_id` of the parent namespace.
+       var parent: String = "" is writable
+
+       # The classes defined in the module.
+       var inner_classes: SimpleCollection[String] = new Array[String]
+
+       init do
+               super
+               self.labels.add("MModule")
+       end
+
+       redef fun declare_class(id: String, name: String) do
+               inner_classes.add(id)
+       end
+
+       redef fun put_edges do
+               graph.add_edge(graph.by_id[parent], "DECLARES", self)
+               for c in inner_classes do
+                       var node = graph.by_id[c].as(ClassCompound)
+                       graph.add_edge(self, "INTRODUCES", node)
+                       graph.add_edge(self, "DEFINES", node.class_def)
+               end
+       end
+end
diff --git a/contrib/neo_doxygen/src/neo_doxygen.nit b/contrib/neo_doxygen/src/neo_doxygen.nit
new file mode 100644 (file)
index 0000000..4ce124b
--- /dev/null
@@ -0,0 +1,135 @@
+# 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.
+
+# Doxygen XML to Neo4j
+#
+# ## Synopsis
+#
+#     neo_doxygen project_name xml_output_dir [neo4j_url]
+#
+# ## Description
+#
+# Convert a Doxygen XML output into a model in Neo4j that is readable by the
+# `nx` tool.
+#
+# ## Arguments
+#
+# * project_name: The internal name of the project. Must the same name as the
+# one specified to the `nx` tool.
+#
+# * xml_output_dir: The directory where the XML documents generated by Doxygen
+# are located.
+#
+# * neo4j_url: The URL of the instance of Neo4j to use.
+# `http://localhost:7474` by default.
+module neo_doxygen
+
+import model
+import doxml
+
+# An importation task.
+class NeoDoxygen
+       var client: Neo4jClient
+       var model: ProjectGraph is noinit
+
+       # How many operation can be executed in one batch?
+       private var batch_max_size = 1000
+
+       # Generate a graph from the specified project model.
+       #
+       # Parameters:
+       #
+       # * `name`: project name.
+       # * `dir`: Doxygen XML output directory path.
+       fun put_project(name: String, dir: String) do
+               model = new ProjectGraph(name)
+               var reader = new CompoundFileReader(model)
+               # Queue for sub-directories.
+               var directories = new Array[String]
+
+               if dir.length > 1 and dir.chars.last == "/" then
+                       dir = dir.substring(0, dir.length - 1)
+               end
+               loop
+                       for f in dir.files do
+                               var path = dir/f
+                               if path.file_stat.is_dir then
+                                       directories.push(path)
+                               else if f.has_suffix(".xml") and f != "index.xml" then
+                                       print "Processing {path}..."
+                                       reader.read(path)
+                               end
+                       end
+                       if directories.length <= 0 then break
+                       dir = directories.pop
+               end
+       end
+
+       # Save the graph.
+       fun save do
+               model.put_edges
+               var nodes = model.all_nodes
+               print("Saving {nodes.length} nodes...")
+               push_all(nodes)
+               var edges = model.all_edges
+               print("Saving {edges.length} edges...")
+               push_all(edges)
+       end
+
+       # Save `neo_entities` in the database using batch mode.
+       private fun push_all(neo_entities: Collection[NeoEntity]) do
+               var batch = new NeoBatch(client)
+               var len = neo_entities.length
+               var sum = 0
+               var i = 1
+
+               for nentity in neo_entities do
+                       batch.save_entity(nentity)
+                       if i == batch_max_size then
+                               do_batch(batch)
+                               sum += batch_max_size
+                               print("\t{sum * 100 / len}% done.")
+                               batch = new NeoBatch(client)
+                               i = 1
+                       else
+                               i += 1
+                       end
+               end
+               do_batch(batch)
+       end
+
+       # Execute `batch` and check for errors.
+       #
+       # Abort if `batch.execute` returns errors.
+       private fun do_batch(batch: NeoBatch) do
+               var errors = batch.execute
+               if not errors.is_empty then
+                       for e in errors do sys.stderr.write("{sys.program_name}: {e}\n")
+                       exit(1)
+               end
+       end
+end
+
+if args.length != 2 and args.length != 3 then
+       stderr.write("Usage: {sys.program_name} project_name xml_output_dir [neo4j_url]\n")
+       exit(1)
+end
+var url = "http://localhost:7474"
+if args.length >= 3 then
+       url = args[2]
+end
+
+var neo = new NeoDoxygen(new Neo4jClient(url))
+neo.put_project(args[0], args[1])
+neo.save