--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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