This is an important refactor of the way documentation generation works

* Installs luarocks WITH it's executable (easy to install other rocks if necessary)
* Use Lua supplied with luarocks
* Create Utils/luadocumentor.bat, which works with RELATIVE PATH ! -> Everybody can generate the doc
* Updated launch files accordingly
This commit is contained in:
Grey-Echo
2017-04-05 01:26:39 +02:00
parent 513a103947
commit 3b69cf992e
313 changed files with 16547 additions and 36807 deletions

View File

@@ -0,0 +1,241 @@
--------------------------------------------------------------------------------
-- Copyright (c) 2011-2012 Sierra Wireless.
-- All rights reserved. This program and the accompanying materials
-- are made available under the terms of the Eclipse Public License v1.0
-- which accompanies this distribution, and is available at
-- http://www.eclipse.org/legal/epl-v10.html
--
-- Contributors:
-- Simon BERNARD <sbernard@sierrawireless.com>
-- - initial API and implementation and initial documentation
--------------------------------------------------------------------------------
local M = {}
--------------------------------------------------------------------------------
-- API MODEL
--------------------------------------------------------------------------------
function M._file()
local file = {
-- FIELDS
tag = "file",
name = nil, -- string
shortdescription = "", -- string
description = "", -- string
types = {}, -- map from typename to type
globalvars = {}, -- map from varname to item
returns = {}, -- list of return
-- FUNCTIONS
addtype = function (self,type)
self.types[type.name] = type
type.parent = self
end,
mergetype = function (self,newtype,erase,erasesourcerangefield)
local currenttype = self.types[newtype.name]
if currenttype then
-- merge recordtypedef
if currenttype.tag =="recordtypedef" and newtype.tag == "recordtypedef" then
-- merge fields
for fieldname ,field in pairs( newtype.fields) do
local currentfield = currenttype.fields[fieldname]
if erase or not currentfield then
currenttype:addfield(field)
elseif erasesourcerangefield then
if field.sourcerange.min and field.sourcerange.max then
currentfield.sourcerange.min = field.sourcerange.min
currentfield.sourcerange.max = field.sourcerange.max
end
end
end
-- merge descriptions and source ranges
if erase then
if newtype.description or newtype.description == "" then currenttype.description = newtype.description end
if newtype.shortdescription or newtype.shortdescription == "" then currenttype.shortdescription = newtype.shortdescription end
if newtype.sourcerange.min and newtype.sourcerange.max then
currenttype.sourcerange.min = newtype.sourcerange.min
currenttype.sourcerange.max = newtype.sourcerange.max
end
end
-- merge functiontypedef
elseif currenttype.tag == "functiontypedef" and newtype.tag == "functiontypedef" then
-- merge params
for i, param1 in ipairs(newtype.params) do
local missing = true
for j, param2 in ipairs(currenttype.params) do
if param1.name == param2.name then
missing = false
break
end
end
if missing then
table.insert(currenttype.params,param1)
end
end
-- merge descriptions and source ranges
if erase then
if newtype.description or newtype.description == "" then currenttype.description = newtype.description end
if newtype.shortdescription or newtype.shortdescription == "" then currenttype.shortdescription = newtype.shortdescription end
if newtype.sourcerange.min and newtype.sourcerange.max then
currenttype.sourcerange.min = newtype.sourcerange.min
currenttype.sourcerange.max = newtype.sourcerange.max
end
end
end
else
self:addtype(newtype)
end
end,
addglobalvar = function (self,item)
self.globalvars[item.name] = item
item.parent = self
end,
moduletyperef = function (self)
if self and self.returns[1] and self.returns[1].types[1] then
local typeref = self.returns[1].types[1]
return typeref
end
end
}
return file
end
function M._recordtypedef(name)
local recordtype = {
-- FIELDS
tag = "recordtypedef",
name = name, -- string (mandatory)
shortdescription = "", -- string
description = "", -- string
fields = {}, -- map from fieldname to field
sourcerange = {min=0,max=0},
-- FUNCTIONS
addfield = function (self,field)
self.fields[field.name] = field
field.parent = self
end
}
return recordtype
end
function M._functiontypedef(name)
return {
tag = "functiontypedef",
name = name, -- string (mandatory)
shortdescription = "", -- string
description = "", -- string
params = {}, -- list of parameter
returns = {} -- list of return
}
end
function M._parameter(name)
return {
tag = "parameter",
name = name, -- string (mandatory)
description = "", -- string
type = nil -- typeref (external or internal or primitive typeref)
}
end
function M._item(name)
return {
-- FIELDS
tag = "item",
name = name, -- string (mandatory)
shortdescription = "", -- string
description = "", -- string
type = nil, -- typeref (external or internal or primitive typeref)
occurrences = {}, -- list of identifier (see internalmodel)
sourcerange = {min=0, max=0},
-- This is A TRICK
-- This value is ALWAYS nil, except for internal purposes (short references).
external = nil,
-- FUNCTIONS
addoccurence = function (self,occ)
table.insert(self.occurrences,occ)
occ.definition = self
end,
resolvetype = function (self,file)
if self and self.type then
if self.type.tag =="internaltyperef" then
-- if file is not given try to retrieve it.
if not file then
if self.parent and self.parent.tag == 'recordtypedef' then
file = self.parent.parent
elseif self.parent.tag == 'file' then
file = self.parent
end
end
if file then return file.types[self.type.typename] end
elseif self.type.tag =="inlinetyperef" then
return self.type.def
end
end
end
}
end
function M._externaltypref(modulename, typename)
return {
tag = "externaltyperef",
modulename = modulename, -- string
typename = typename -- string
}
end
function M._internaltyperef(typename)
return {
tag = "internaltyperef",
typename = typename -- string
}
end
function M._primitivetyperef(typename)
return {
tag = "primitivetyperef",
typename = typename -- string
}
end
function M._moduletyperef(modulename,returnposition)
return {
tag = "moduletyperef",
modulename = modulename, -- string
returnposition = returnposition -- number
}
end
function M._exprtyperef(expression,returnposition)
return {
tag = "exprtyperef",
expression = expression, -- expression (see internal model)
returnposition = returnposition -- number
}
end
function M._inlinetyperef(definition)
return {
tag = "inlinetyperef",
def = definition, -- expression (see internal model)
}
end
function M._return(description)
return {
tag = "return",
description = description or "", -- string
types = {} -- list of typref (external or internal or primitive typeref)
}
end
return M

View File

@@ -0,0 +1,459 @@
--------------------------------------------------------------------------------
-- Copyright (c) 2011-2012 Sierra Wireless.
-- All rights reserved. This program and the accompanying materials
-- are made available under the terms of the Eclipse Public License v1.0
-- which accompanies this distribution, and is available at
-- http://www.eclipse.org/legal/epl-v10.html
--
-- Contributors:
-- Simon BERNARD <sbernard@sierrawireless.com>
-- - initial API and implementation and initial documentation
--------------------------------------------------------------------------------
local apimodel = require "models.apimodel"
local ldp = require "models.ldparser"
local Q = require "metalua.treequery"
local M = {}
local handledcomments={} -- cache to know the comment already handled
----
-- UTILITY METHODS
local primitivetypes = {
['boolean'] = true,
['function'] = true,
['nil'] = true,
['number'] = true,
['string'] = true,
['table'] = true,
['thread'] = true,
['userdata'] = true
}
-- get or create the typedef with the name "name"
local function gettypedef(_file,name,kind,sourcerangemin,sourcerangemax)
local kind = kind or "recordtypedef"
local _typedef = _file.types[name]
if _typedef then
if _typedef.tag == kind then return _typedef end
else
if kind == "recordtypedef" and name ~= "global" then
local _recordtypedef = apimodel._recordtypedef(name)
-- define sourcerange
_recordtypedef.sourcerange.min = sourcerangemin
_recordtypedef.sourcerange.max = sourcerangemax
-- add to file if a name is defined
if _recordtypedef.name then _file:addtype(_recordtypedef) end
return _recordtypedef
elseif kind == "functiontypedef" then
-- TODO support function
return nil
else
return nil
end
end
return nil
end
-- create a typeref from the typref doc_tag
local function createtyperef(dt_typeref,_file,sourcerangemin,sourcerangemax)
local _typeref
if dt_typeref.tag == "typeref" then
if dt_typeref.module then
-- manage external type
_typeref = apimodel._externaltypref()
_typeref.modulename = dt_typeref.module
_typeref.typename = dt_typeref.type
else
if primitivetypes[dt_typeref.type] then
-- manage primitive type
_typeref = apimodel._primitivetyperef()
_typeref.typename = dt_typeref.type
else
-- manage internal type
_typeref = apimodel._internaltyperef()
_typeref.typename = dt_typeref.type
if _file then
gettypedef(_file, _typeref.typename, "recordtypedef", sourcerangemin,sourcerangemax)
end
end
end
end
return _typeref
end
-- create a return from the return doc_tag
local function createreturn(dt_return,_file,sourcerangemin,sourcerangemax)
local _return = apimodel._return()
_return.description = dt_return.description
-- manage typeref
if dt_return.types then
for _, dt_typeref in ipairs(dt_return.types) do
local _typeref = createtyperef(dt_typeref,_file,sourcerangemin,sourcerangemax)
if _typeref then
table.insert(_return.types,_typeref)
end
end
end
return _return
end
-- create a item from the field doc_tag
local function createfield(dt_field,_file,sourcerangemin,sourcerangemax)
local _item = apimodel._item(dt_field.name)
if dt_field.shortdescription then
_item.shortdescription = dt_field.shortdescription
_item.description = dt_field.description
else
_item.shortdescription = dt_field.description
end
-- manage typeref
local dt_typeref = dt_field.type
if dt_typeref then
_item.type = createtyperef(dt_typeref,_file,sourcerangemin,sourcerangemax)
end
return _item
end
-- create a param from the param doc_tag
local function createparam(dt_param,_file,sourcerangemin,sourcerangemax)
if not dt_param.name then return nil end
local _parameter = apimodel._parameter(dt_param.name)
_parameter.description = dt_param.description
-- manage typeref
local dt_typeref = dt_param.type
if dt_typeref then
_parameter.type = createtyperef(dt_typeref,_file,sourcerangemin,sourcerangemax)
end
return _parameter
end
-- get or create the typedef with the name "name"
function M.additemtoparent(_file,_item,scope,sourcerangemin,sourcerangemax)
if scope and not scope.module then
if _item.name then
if scope.type == "global" then
_file:addglobalvar(_item)
else
local _recordtypedef = gettypedef (_file, scope.type ,"recordtypedef",sourcerangemin,sourcerangemax)
_recordtypedef:addfield(_item)
end
else
-- if no item name precise we store the scope in the item to be able to add it to the right parent later
_item.scope = scope
end
end
end
-- Function type counter
local i = 0
-- Reset function type counter
local function resetfunctiontypeidgenerator()
i = 0
end
-- Provides an unique index for a function type
local function generatefunctiontypeid()
i = i + 1
return i
end
-- generate a function type name
local function generatefunctiontypename(_functiontypedef)
local name = {"__"}
if _functiontypedef.returns and _functiontypedef.returns[1] then
local ret = _functiontypedef.returns[1]
for _, type in ipairs(ret.types) do
if type.typename then
if type.modulename then
table.insert(name,type.modulename)
end
table.insert(name,"#")
table.insert(name,type.typename)
end
end
end
table.insert(name,"=")
if _functiontypedef.params then
for _, param in ipairs(_functiontypedef.params) do
local type = param.type
if type then
if type.typename then
if type.modulename then
table.insert(name,type.modulename)
end
table.insert(name,"#")
table.insert(name,type.typename)
else
table.insert(name,"#unknown")
end
end
table.insert(name,"[")
table.insert(name,param.name)
table.insert(name,"]")
end
end
table.insert(name,"__")
table.insert(name, generatefunctiontypeid())
return table.concat(name)
end
------------------------------------------------------
-- create the module api
function M.createmoduleapi(ast,modulename)
-- Initialise function type naming
resetfunctiontypeidgenerator()
local _file = apimodel._file()
local _comment2apiobj = {}
local function handlecomment(comment)
-- Extract information from tagged comments
local parsedcomment = ldp.parse(comment[1])
if not parsedcomment then return nil end
-- Get tags from the languages
local regulartags = parsedcomment.tags
-- Will contain last API object generated from comments
local _lastapiobject
-- if comment is an ld comment
if regulartags then
-- manage "module" comment
if regulartags["module"] then
-- get name
_file.name = regulartags["module"][1].name or modulename
_lastapiobject = _file
-- manage descriptions
_file.shortdescription = parsedcomment.shortdescription
_file.description = parsedcomment.description
local sourcerangemin = comment.lineinfo.first.offset
local sourcerangemax = comment.lineinfo.last.offset
-- manage returns
if regulartags ["return"] then
for _, dt_return in ipairs(regulartags ["return"]) do
local _return = createreturn(dt_return,_file,sourcerangemin,sourcerangemax)
table.insert(_file.returns,_return)
end
end
-- if no returns on module create a defaultreturn of type #modulename
if #_file.returns == 0 and _file.name then
-- create internal type ref
local _typeref = apimodel._internaltyperef()
_typeref.typename = _file.name
-- create return
local _return = apimodel._return()
table.insert(_return.types,_typeref)
-- add return
table.insert(_file.returns,_return)
--create recordtypedef is not define
gettypedef(_file,_typeref.typename,"recordtypedef",sourcerangemin,sourcerangemax)
end
-- manage "type" comment
elseif regulartags["type"] and regulartags["type"][1].name ~= "global" then
local dt_type = regulartags["type"][1];
-- create record type if it doesn't exist
local sourcerangemin = comment.lineinfo.first.offset
local sourcerangemax = comment.lineinfo.last.offset
local _recordtypedef = gettypedef (_file, dt_type.name ,"recordtypedef",sourcerangemin,sourcerangemax)
_lastapiobject = _recordtypedef
-- re-set sourcerange in case the type was created before the type tag
_recordtypedef.sourcerange.min = sourcerangemin
_recordtypedef.sourcerange.max = sourcerangemax
-- manage description
_recordtypedef.shortdescription = parsedcomment.shortdescription
_recordtypedef.description = parsedcomment.description
-- manage fields
if regulartags["field"] then
for _, dt_field in ipairs(regulartags["field"]) do
local _item = createfield(dt_field,_file,sourcerangemin,sourcerangemax)
-- define sourcerange only if we create it
_item.sourcerange.min = sourcerangemin
_item.sourcerange.max = sourcerangemax
if _item then _recordtypedef:addfield(_item) end
end
end
elseif regulartags["field"] then
local dt_field = regulartags["field"][1]
-- create item
local sourcerangemin = comment.lineinfo.first.offset
local sourcerangemax = comment.lineinfo.last.offset
local _item = createfield(dt_field,_file,sourcerangemin,sourcerangemax)
_item.shortdescription = parsedcomment.shortdescription
_item.description = parsedcomment.description
_lastapiobject = _item
-- define sourcerange
_item.sourcerange.min = sourcerangemin
_item.sourcerange.max = sourcerangemax
-- add item to its parent
local scope = regulartags["field"][1].parent
M.additemtoparent(_file,_item,scope,sourcerangemin,sourcerangemax)
elseif regulartags["function"] or regulartags["param"] or regulartags["return"] then
-- create item
local _item = apimodel._item()
_item.shortdescription = parsedcomment.shortdescription
_item.description = parsedcomment.description
_lastapiobject = _item
-- set name
if regulartags["function"] then _item.name = regulartags["function"][1].name end
-- define sourcerange
local sourcerangemin = comment.lineinfo.first.offset
local sourcerangemax = comment.lineinfo.last.offset
_item.sourcerange.min = sourcerangemin
_item.sourcerange.max = sourcerangemax
-- create function type
local _functiontypedef = apimodel._functiontypedef()
_functiontypedef.shortdescription = parsedcomment.shortdescription
_functiontypedef.description = parsedcomment.description
-- manage params
if regulartags["param"] then
for _, dt_param in ipairs(regulartags["param"]) do
local _param = createparam(dt_param,_file,sourcerangemin,sourcerangemax)
table.insert(_functiontypedef.params,_param)
end
end
-- manage returns
if regulartags["return"] then
for _, dt_return in ipairs(regulartags["return"]) do
local _return = createreturn(dt_return,_file,sourcerangemin,sourcerangemax)
table.insert(_functiontypedef.returns,_return)
end
end
-- add type name
_functiontypedef.name = generatefunctiontypename(_functiontypedef)
_file:addtype(_functiontypedef)
-- create ref to this type
local _internaltyperef = apimodel._internaltyperef()
_internaltyperef.typename = _functiontypedef.name
_item.type=_internaltyperef
-- add item to its parent
local sourcerangemin = comment.lineinfo.first.offset
local sourcerangemax = comment.lineinfo.last.offset
local scope = (regulartags["function"] and regulartags["function"][1].parent) or nil
M.additemtoparent(_file,_item,scope,sourcerangemin,sourcerangemax)
end
end
-- when we could not know which type of api object it is, we suppose this is an item
if not _lastapiobject then
_lastapiobject = apimodel._item()
_lastapiobject.shortdescription = parsedcomment.shortdescription
_lastapiobject.description = parsedcomment.description
_lastapiobject.sourcerange.min = comment.lineinfo.first.offset
_lastapiobject.sourcerange.max = comment.lineinfo.last.offset
end
--
-- Store user defined tags
--
local thirdtags = parsedcomment and parsedcomment.unknowntags
if thirdtags then
-- Define a storage index for user defined tags on current API element
if not _lastapiobject.metadata then _lastapiobject.metadata = {} end
-- Loop over user defined tags
for usertag, taglist in pairs(thirdtags) do
if not _lastapiobject.metadata[ usertag ] then
_lastapiobject.metadata[ usertag ] = {
tag = usertag
}
end
for _, tag in ipairs( taglist ) do
table.insert(_lastapiobject.metadata[usertag], tag)
end
end
end
-- if we create an api object linked it to
_comment2apiobj[comment] =_lastapiobject
end
local function parsecomment(node, parent, ...)
-- check for comments before this node
if node.lineinfo and node.lineinfo.first.comments then
local comments = node.lineinfo.first.comments
-- check all comments
for _,comment in ipairs(comments) do
-- if not already handled
if not handledcomments[comment] then
handlecomment(comment)
handledcomments[comment]=true
end
end
end
-- check for comments after this node
if node.lineinfo and node.lineinfo.last.comments then
local comments = node.lineinfo.last.comments
-- check all comments
for _,comment in ipairs(comments) do
-- if not already handled
if not handledcomments[comment] then
handlecomment(comment)
handledcomments[comment]=true
end
end
end
end
Q(ast):filter(function(x) return x.tag~=nil end):foreach(parsecomment)
return _file, _comment2apiobj
end
function M.extractlocaltype ( commentblock,_file)
if not commentblock then return nil end
local stringcomment = commentblock[1]
local parsedtag = ldp.parseinlinecomment(stringcomment)
if parsedtag then
local sourcerangemin = commentblock.lineinfo.first.offset
local sourcerangemax = commentblock.lineinfo.last.offset
return createtyperef(parsedtag,_file,sourcerangemin,sourcerangemax), parsedtag.description
end
return nil, stringcomment
end
M.generatefunctiontypename = generatefunctiontypename
return M

View File

@@ -0,0 +1,65 @@
--------------------------------------------------------------------------------
-- Copyright (c) 2012 Sierra Wireless.
-- All rights reserved. This program and the accompanying materials
-- are made available under the terms of the Eclipse Public License v1.0
-- which accompanies this distribution, and is available at
-- http://www.eclipse.org/legal/epl-v10.html
--
-- Contributors:
-- Kevin KIN-FOO <kkinfoo@sierrawireless.com>
-- - initial API and implementation and initial documentation
--------------------------------------------------------------------------------
local M = {}
function M._internalcontent()
return {
content = nil, -- block
unknownglobalvars = {}, -- list of item
tag = "MInternalContent"
}
end
function M._block()
return {
content = {}, -- list of expr (identifier, index, call, invoke, block)
localvars = {}, -- list of {var=item, scope ={min,max}}
sourcerange = {min=0,max=0},
tag = "MBlock"
}
end
function M._identifier()
return {
definition = nil, -- item
sourcerange = {min=0,max=0},
tag = "MIdentifier"
}
end
function M._index(key, value)
return {
left= key, -- expr (identifier, index, call, invoke, block)
right= value, -- string
sourcerange = {min=0,max=0},
tag = "MIndex"
}
end
function M._call(funct)
return {
func = funct, -- expr (identifier, index, call, invoke, block)
sourcerange = {min=0,max=0},
tag = "MCall"
}
end
function M._invoke(name, expr)
return {
functionname = name, -- string
record = expr, -- expr (identifier, index, call, invoke, block)
sourcerange = {min=0,max=0},
tag = "MInvoke"
}
end
return M

View File

@@ -0,0 +1,861 @@
--------------------------------------------------------------------------------
-- Copyright (c) 2011-2012 Sierra Wireless.
-- All rights reserved. This program and the accompanying materials
-- are made available under the terms of the Eclipse Public License v1.0
-- which accompanies this distribution, and is available at
-- http://www.eclipse.org/legal/epl-v10.html
--
-- Contributors:
-- Simon BERNARD <sbernard@sierrawireless.com>
-- - initial API and implementation and initial documentation
--------------------------------------------------------------------------------
-{ extension ('match', ...) }
local Q = require 'metalua.treequery'
local internalmodel = require 'models.internalmodel'
local apimodel = require 'models.apimodel'
local apimodelbuilder = require 'models.apimodelbuilder'
local M = {}
-- Analyzes an AST and returns two tables
-- * `locals`, which associates `Id{ } nodes which create a local variable
-- to a list of the `Id{ } occurrence nodes of that variable;
-- * `globals` which associates variable names to occurrences of
-- global variables having that name.
function bindings(ast)
local locals, globals = { }, { }
local function f(id, ...)
local name = id[1]
if Q.is_binder(id, ...) then
local binder = ... -- parent is the binder
locals[binder] = locals[binder] or { }
locals[binder][name]={ }
else
local _, binder = Q.get_binder(id, ...)
if binder then -- this is a local
table.insert(locals[binder][name], id)
else
local g = globals[name]
if g then table.insert(g, id) else globals[name]={id} end
end
end
end
Q(ast) :filter('Id') :foreach(f)
return locals, globals
end
-- --------------------------------------
-- ----------------------------------------------------------
-- return the comment linked before to this node
-- ----------------------------------------------------------
local function getlinkedcommentbefore(node)
local function _getlinkedcomment(node,line)
if node and node.lineinfo and node.lineinfo.first.line == line then
-- get the last comment before (the nearest of code)
local comments = node.lineinfo.first.comments
local comment = comments and comments[#comments]
if comment and comment.lineinfo.last.line == line-1 then
-- ignore the comment if there are code before on the same line
if node.lineinfo.first.facing and (node.lineinfo.first.facing.line ~= comment.lineinfo.first.line) then
return comment
end
else
return _getlinkedcomment(node.parent,line)
end
end
return nil
end
if node.lineinfo and node.lineinfo.first.line then
return _getlinkedcomment(node,node.lineinfo.first.line)
else
return nil
end
end
-- ----------------------------------------------------------
-- return the comment linked after to this node
-- ----------------------------------------------------------
local function getlinkedcommentafter(node)
local function _getlinkedcomment(node,line)
if node and node.lineinfo and node.lineinfo.last.line == line then
-- get the first comment after (the nearest of code)
local comments = node.lineinfo.last.comments
local comment = comments and comments[1]
if comment and comment.lineinfo.first.line == line then
return comment
else
return _getlinkedcomment(node.parent,line)
end
end
return nil
end
if node.lineinfo and node.lineinfo.last.line then
return _getlinkedcomment(node,node.lineinfo.last.line)
else
return nil
end
end
-- ----------------------------------------------------------
-- return true if this node is a block for the internal representation
-- ----------------------------------------------------------
local supported_b = {
Function = true,
Do = true,
While = true,
Fornum = true,
Forin = true,
Repeat = true,
}
local function supportedblock(node, parent)
return supported_b[ node.tag ] or
(parent and parent.tag == "If" and node.tag == nil)
end
-- ----------------------------------------------------------
-- create a block from the metalua node
-- ----------------------------------------------------------
local function createblock(block, parent)
local _block = internalmodel._block()
match block with
| `Function{param, body}
| `Do{...}
| `Fornum {identifier, min, max, body}
| `Forin {identifiers, exprs, body}
| `Repeat {body, expr} ->
_block.sourcerange.min = block.lineinfo.first.offset
_block.sourcerange.max = block.lineinfo.last.offset
| `While {expr, body} ->
_block.sourcerange.min = body.lineinfo.first.facing.offset
_block.sourcerange.max = body.lineinfo.last.facing.offset
| _ ->
if parent and parent.tag == "If" and block.tag == nil then
_block.sourcerange.min = block.lineinfo.first.facing.offset
_block.sourcerange.max = block.lineinfo.last.facing.offset
end
end
return _block
end
-- ----------------------------------------------------------
-- return true if this node is a expression in the internal representation
-- ----------------------------------------------------------
local supported_e = {
Index = true,
Id = true,
Call = true,
Invoke = true
}
local function supportedexpr(node)
return supported_e[ node.tag ]
end
local idto_block = {} -- cache from metalua id to internal model block
local idto_identifier = {} -- cache from metalua id to internal model indentifier
local expreto_expression = {} -- cache from metalua expression to internal model expression
-- ----------------------------------------------------------
-- create an expression from a metalua node
-- ----------------------------------------------------------
local function createexpr(expr,_block)
local _expr = nil
match expr with
| `Id { name } ->
-- we store the block which hold this node
-- to be able to define
idto_block[expr]= _block
-- if expr has not line info, it means expr has no representation in the code
-- so we don't need it.
if not expr.lineinfo then return nil end
-- create identifier
local _identifier = internalmodel._identifier()
idto_identifier[expr]= _identifier
_expr = _identifier
| `Index { innerexpr, `String{fieldname} } ->
if not expr.lineinfo then return nil end
-- create index
local _expression = createexpr(innerexpr,_block)
if _expression then _expr = internalmodel._index(_expression,fieldname) end
| `Call{innerexpr, ...} ->
if not expr.lineinfo then return nil end
-- create call
local _expression = createexpr(innerexpr,_block)
if _expression then _expr = internalmodel._call(_expression) end
| `Invoke{innerexpr,`String{functionname},...} ->
if not expr.lineinfo then return nil end
-- create invoke
local _expression = createexpr(innerexpr,_block)
if _expression then _expr = internalmodel._invoke(functionname,_expression) end
| _ ->
end
if _expr then
_expr.sourcerange.min = expr.lineinfo.first.offset
_expr.sourcerange.max = expr.lineinfo.last.offset
expreto_expression[expr] = _expr
end
return _expr
end
-- ----------------------------------------------------------
-- create block and expression node
-- ----------------------------------------------------------
local function createtreestructure(ast)
-- create internal content
local _internalcontent = internalmodel._internalcontent()
-- create root block
local _block = internalmodel._block()
local _blocks = { _block }
_block.sourcerange.min = ast.lineinfo.first.facing.offset
-- TODO remove the math.max when we support partial AST
_block.sourcerange.max = math.max(ast.lineinfo.last.facing.offset, 10000)
_internalcontent.content = _block
-- visitor function (down)
local function down (node,parent)
if supportedblock(node,parent) then
-- create the block
local _block = createblock(node,parent)
-- add it to parent block
table.insert(_blocks[#_blocks].content, _block)
-- enqueue the last block to know the "current" block
table.insert(_blocks,_block)
elseif supportedexpr(node) then
-- we handle expression only if it was not already do
if not expreto_expression[node] then
-- create expr
local _expression = createexpr(node,_blocks[#_blocks])
-- add it to parent block
if _expression then
table.insert(_blocks[#_blocks].content, _expression)
end
end
end
end
-- visitor function (up)
local function up (node, parent)
if supportedblock(node,parent) then
-- dequeue the last block to know the "current" block
table.remove(_blocks,#_blocks)
end
end
-- visit ast and build internal model
Q(ast):foreach(down,up)
return _internalcontent
end
local getitem
-- ----------------------------------------------------------
-- create the type from the node and position
-- ----------------------------------------------------------
local function createtype(node,position,comment2apiobj,file)
-- create module type ref
match node with
| `Call{ `Id "require", `String {modulename}} ->
return apimodel._moduletyperef(modulename,position)
| `Function {params, body} ->
-- create the functiontypedef from code
local _functiontypedef = apimodel._functiontypedef()
for _, p in ipairs(params) do
-- create parameters
local paramname
if p.tag=="Dots" then
paramname = "..."
else
paramname = p[1]
end
local _param = apimodel._parameter(paramname)
table.insert(_functiontypedef.params,_param)
end
_functiontypedef.name = "___" -- no name for inline type
return apimodel._inlinetyperef(_functiontypedef)
| `String {value} ->
local typeref = apimodel._primitivetyperef("string")
return typeref
| `Number {value} ->
local typeref = apimodel._primitivetyperef("number")
return typeref
| `True | `False ->
local typeref = apimodel._primitivetyperef("boolean")
return typeref
| `Table {...} ->
-- create recordtypedef from code
local _recordtypedef = apimodel._recordtypedef("___") -- no name for inline type
-- for each element of the table
for i=1,select("#", ...) do
local pair = select(i, ...)
-- if this is a pair we create a new item in the type
if pair.tag == "Pair" then
-- create an item
local _item = getitem(pair,nil, comment2apiobj,file)
if _item then
_recordtypedef:addfield(_item)
end
end
end
return apimodel._inlinetyperef(_recordtypedef)
| _ ->
end
-- if node is an expression supported
local supportedexpr = expreto_expression[node]
if supportedexpr then
-- create expression type ref
return apimodel._exprtyperef(supportedexpr,position)
end
end
local function completeapidoctype(apidoctype,itemname,init,file,comment2apiobj)
if not apidoctype.name then
apidoctype.name = itemname
file:mergetype(apidoctype)
end
-- create type from code
local typeref = createtype(init,1,comment2apiobj,file)
if typeref and typeref.tag == "inlinetyperef"
and typeref.def.tag == "recordtypedef" then
-- set the name
typeref.def.name = apidoctype.name
-- merge the type with priority to documentation except for source range
file:mergetype(typeref.def,false,true)
end
end
local function completeapidocitem (apidocitem, itemname, init, file, binder, comment2apiobj)
-- manage the case item has no name
if not apidocitem.name then
apidocitem.name = itemname
-- if item has no name this means it could not be attach to a parent
if apidocitem.scope then
apimodelbuilder.additemtoparent(file,apidocitem,apidocitem.scope,apidocitem.sourcerange.min,apidocitem.sourcerange.max)
apidocitem.scope = nil
end
end
-- for function try to merge definition
local apitype = apidocitem:resolvetype(file)
if apitype and apitype.tag == "functiontypedef" then
local codetype = createtype(init,1,comment2apiobj,file)
if codetype and codetype.tag =="inlinetyperef" then
codetype.def.name = apitype.name
file:mergetype(codetype.def)
end
end
-- manage the case item has no type
if not apidocitem.type then
-- extract typing from comment
local type, desc = apimodelbuilder.extractlocaltype(getlinkedcommentafter(binder),file)
if type then
apidocitem.type = type
else
-- if not found extracttype from code
apidocitem.type = createtype(init,1,comment2apiobj,file)
end
end
end
-- ----------------------------------------------------------
-- create or get the item finding in the binder with the given itemname
-- return also the ast node corresponding to this item
-- ----------------------------------------------------------
getitem = function (binder, itemname, comment2apiobj, file)
-- local function to create item
local function createitem(itemname, astnode, itemtype, description)
local _item = apimodel._item(itemname)
if description then _item.description = description end
_item.type = itemtype
if astnode and astnode.lineinfo then
_item.sourcerange.min = astnode.lineinfo.first.offset
_item.sourcerange.max = astnode.lineinfo.last.offset
end
return _item, astnode
end
-- try to match binder with known patter of item declaration
match binder with
| `Pair {string, init}
| `Set { {`Index { right , string}}, {init,...}} if string and string.tag =="String" ->
-- Pair and set is for searching field from type ..
-- if the itemname is given this mean we search for a local or a global not a field type.
if not itemname then
local itemname = string[1]
-- check for luadoc typing
local commentbefore = getlinkedcommentbefore(binder)
local apiobj = comment2apiobj[commentbefore] -- find apiobj linked to this comment
if apiobj then
if apiobj.tag=="item" then
if not apiobj.name or apiobj.name == itemname then
-- use code to complete api information if it's necessary
completeapidocitem(apiobj, itemname, init,file,binder,comment2apiobj)
-- for item use code source range rather than doc source range
if string and string.lineinfo then
apiobj.sourcerange.min = string.lineinfo.first.offset
apiobj.sourcerange.max = string.lineinfo.last.offset
end
return apiobj, string
end
elseif apiobj.tag=="recordtypedef" then
-- use code to complete api information if it's necessary
completeapidoctype(apiobj, itemname, init,file,comment2apiobj)
return createitem(itemname, string, apimodel._internaltyperef(apiobj.name), nil)
end
-- if the apiobj could not be associated to the current obj,
-- we do not use the documentation neither
commentbefore = nil
end
-- else we use code to extract the type and description
-- check for "local" typing
local type, desc = apimodelbuilder.extractlocaltype(getlinkedcommentafter(binder),file)
local desc = desc or (commentbefore and commentbefore[1])
if type then
return createitem(itemname, string, type, desc )
else
-- if no "local typing" extract type from code
return createitem(itemname, string, createtype(init,1,comment2apiobj,file), desc)
end
end
| `Set {ids, inits}
| `Local {ids, inits} ->
-- if this is a single local var declaration
-- we check if there are a comment block linked and try to extract the type
if #ids == 1 then
local currentid, currentinit = ids[1],inits[1]
-- ignore non Ids node
if currentid.tag ~= 'Id' or currentid[1] ~= itemname then return nil end
-- check for luadoc typing
local commentbefore = getlinkedcommentbefore(binder)
local apiobj = comment2apiobj[commentbefore] -- find apiobj linked to this comment
if apiobj then
if apiobj.tag=="item" then
-- use code to complete api information if it's necessary
if not apiobj.name or apiobj.name == itemname then
completeapidocitem(apiobj, itemname, currentinit,file,binder,comment2apiobj)
-- if this is a global var or if is has no parent
-- we do not create a new item
if not apiobj.parent or apiobj.parent == file then
-- for item use code source range rather than doc source range
if currentid and currentid.lineinfo then
apiobj.sourcerange.min = currentid.lineinfo.first.offset
apiobj.sourcerange.max = currentid.lineinfo.last.offset
end
return apiobj, currentid
else
return createitem(itemname, currentid, apiobj.type, nil)
end
end
elseif apiobj.tag=="recordtypedef" then
-- use code to complete api information if it's necessary
completeapidoctype(apiobj, itemname, currentinit,file,comment2apiobj)
return createitem(itemname, currentid, apimodel._internaltyperef(apiobj.name), nil)
end
-- if the apiobj could not be associated to the current obj,
-- we do not use the documentation neither
commentbefore = nil
end
-- else we use code to extract the type and description
-- check for "local" typing
local type,desc = apimodelbuilder.extractlocaltype(getlinkedcommentafter(binder),file)
desc = desc or (commentbefore and commentbefore[1])
if type then
return createitem(itemname, currentid, type, desc)
else
-- if no "local typing" extract type from code
return createitem(itemname, currentid, createtype(currentinit,1,comment2apiobj,file), desc)
end
end
-- else we use code to extract the type
local init,returnposition = nil,1
for i,id in ipairs(ids) do
-- calculate the current return position
if init and (init.tag == "Call" or init.tag == "Invoke") then
-- if previous init was a call or an invoke
-- we increment the returnposition
returnposition= returnposition+1
else
-- if init is not a function call
-- we change the init used to determine the type
init = inits[i]
end
-- get the name of the current id
local idname = id[1]
-- if this is the good id
if itemname == idname then
-- create type from init node and return position
return createitem (itemname, id, createtype(init,returnposition,comment2apiobj,file),nil)
end
end
| `Function {params, body} ->
for i,id in ipairs(params) do
-- get the name of the current id
local idname = id[1]
-- if this is the good id
if itemname == idname then
-- extract param's type from luadocumentation
local obj = comment2apiobj[getlinkedcommentbefore(binder)]
if obj and obj.tag=="item" then
local typedef = obj:resolvetype(file)
if typedef and typedef.tag =="functiontypedef" then
for j, param in ipairs(typedef.params) do
if i==j then
if i ==1 and itemname == "self" and param.type == nil
and obj.parent and obj.parent.tag == "recordtypedef" and obj.parent.name then
param.type = apimodel._internaltyperef(obj.parent.name)
end
-- TODO perhaps we must clone the typeref
return createitem(itemname,id, param.type,param.description)
end
end
end
end
return createitem(itemname,id)
end
end
| `Forin {ids, expr, body} ->
for i,id in ipairs(ids) do
-- get the name of the current id
local idname = id[1]
-- if this is the good id
if itemname == idname then
-- return data : we can not guess the type for now
return createitem(itemname,id)
end
end
| `Fornum {id, ...} ->
-- get the name of the current id
local idname = id[1]
-- if this is the good id
if itemname == idname then
-- return data : we can not guess the type for now
return createitem(itemname,id)
end
| `Localrec {{id}, {func}} ->
-- get the name of the current id
local idname = id[1]
-- if this is the good id
if itemname == idname then
-- check for luadoc typing
local commentbefore = getlinkedcommentbefore(binder)
local apiobj = comment2apiobj[commentbefore] -- find apiobj linked to this comment
if apiobj then
if apiobj.tag=="item" then
if not apiobj.name or apiobj.name == itemname then
-- use code to complete api information if it's necessary
completeapidocitem(apiobj, itemname, func,file,binder,comment2apiobj)
return createitem(itemname,id,apiobj.type,nil)
end
end
-- if the apiobj could not be associated to the current obj,
-- we do not use the documentation neither
commentbefore = nil
end
-- else we use code to extract the type and description
-- check for "local" typing
local type,desc = apimodelbuilder.extractlocaltype(getlinkedcommentafter(binder),file)
desc = desc or (commentbefore and commentbefore[1])
if type then
return createitem(itemname, id, type, desc)
else
-- if no "local typing" extract type from code
return createitem(itemname, id, createtype(func,1,comment2apiobj,file), desc)
end
end
| _ ->
end
end
-- ----------------------------------------------------------
-- Search from Id node to Set node to find field of type.
--
-- Lua code : table.field1.field2 = 12
-- looks like that in metalua :
-- `Set{
-- `Index { `Index { `Id "table", `String "field1" },
-- `String "field2"},
-- `Number "12"}
-- ----------------------------------------------------------
local function searchtypefield(node,_currentitem,comment2apiobj,file)
-- we are just interested :
-- by item which is field of recordtypedef
-- by ast node which are Index
if _currentitem then
local type = _currentitem:resolvetype(file)
if type and type.tag == "recordtypedef" then
if node and node.tag == "Index" then
local rightpart = node[2]
local _newcurrentitem = type.fields[rightpart[1]]
if _newcurrentitem then
-- if this index represent a known field of the type we continue to search
searchtypefield (node.parent,_newcurrentitem,comment2apiobj,file)
else
-- if not, this is perhaps a new field, but
-- to be a new field this index must be include in a Set
if node.parent and node.parent.tag =="Set" then
-- in this case we create the new item ans add it to the type
local set = node.parent
local item, string = getitem(set,nil, comment2apiobj,file)
-- add this item to the type, only if it has no parent and if this type does not contain already this field
if item and not item.parent and string and not type.fields[string[1]] then
type:addfield(item)
end
end
end
end
end
end
end
-- ----------------------------------------------------------
-- create local vars, global vars and linked it with theirs occurences
-- ----------------------------------------------------------
local function createvardefinitions(_internalcontent,ast,file,comment2apiobj)
-- use bindings to get locals and globals definition
local locals, globals = bindings( ast )
-- create locals var
for binder, namesAndOccurrences in pairs(locals) do
for name, occurrences in pairs(namesAndOccurrences) do
-- get item, id
local _item, id = getitem(binder, name,comment2apiobj,file)
if id then
-- add definition as occurence
-- we consider the identifier in the binder as an occurence
local _identifierdef = idto_identifier[id]
if _identifierdef then
table.insert(_item.occurrences, _identifierdef)
_identifierdef.definition = _item
end
-- add occurences
for _,occurrence in ipairs(occurrences) do
searchtypefield(occurrence.parent, _item,comment2apiobj,file)
local _identifier = idto_identifier[occurrence]
if _identifier then
table.insert(_item.occurrences, _identifier)
_identifier.definition = _item
end
end
-- add item to block
local _block = idto_block[id]
table.insert(_block.localvars,{item=_item,scope = {min=0,max=0}})
end
end
end
-- create globals var
for name, occurrences in pairs( globals ) do
-- get or create definition
local _item = file.globalvars[name]
local binder = occurrences[1].parent
if not _item then
-- global declaration is only if the first occurence in left part of a 'Set'
if binder and binder.tag == "Set" then
_item = getitem(binder, name,comment2apiobj,file)
end
-- if we find and item this is a global var declaration
if _item then
file:addglobalvar(_item)
else
-- else it is an unknown global var
_item = apimodel._item(name)
local _firstoccurrence = idto_identifier[occurrences[1]]
if _firstoccurrence then
_item.sourcerange.min = _firstoccurrence.sourcerange.min
_item.sourcerange.max = _firstoccurrence.sourcerange.max
end
table.insert(_internalcontent.unknownglobalvars,_item)
end
else
-- if the global var definition already exists, we just try to it
if binder then
match binder with
| `Set {ids, inits} ->
-- manage case only if there are 1 element in the Set
if #ids == 1 then
local currentid, currentinit = ids[1],inits[1]
-- ignore non Ids node and bad name
if currentid.tag == 'Id' and currentid[1] == name then
completeapidocitem(_item, name, currentinit,file,binder,comment2apiobj)
if currentid and currentid.lineinfo then
_item.sourcerange.min = currentid.lineinfo.first.offset
_item.sourcerange.max = currentid.lineinfo.last.offset
end
end
end
| _ ->
end
end
end
-- add occurences
for _,occurence in ipairs(occurrences) do
local _identifier = idto_identifier[occurence]
searchtypefield(occurence.parent, _item,comment2apiobj,file)
if _identifier then
table.insert(_item.occurrences, _identifier)
_identifier.definition = _item
end
end
end
end
-- ----------------------------------------------------------
-- add parent to all ast node
-- ----------------------------------------------------------
local function addparents(ast)
-- visitor function (down)
local function down (node,parent)
node.parent = parent
end
-- visit ast and build internal model
Q(ast):foreach(down,up)
end
-- ----------------------------------------------------------
-- try to detect a module declaration from code
-- ----------------------------------------------------------
local function searchmodule(ast,file,comment2apiobj,modulename)
-- if the last statement is a return
if ast then
local laststatement = ast[#ast]
if laststatement and laststatement.tag == "Return" then
-- and if the first expression returned is an identifier.
local firstexpr = laststatement[1]
if firstexpr and firstexpr.tag == "Id" then
-- get identifier in internal model
local _identifier = idto_identifier [firstexpr]
-- the definition should be an inline type
if _identifier
and _identifier.definition
and _identifier.definition.type
and _identifier.definition.type.tag == "inlinetyperef"
and _identifier.definition.type.def.tag == "recordtypedef" then
--set modulename if needed
if not file.name then file.name = modulename end
-- create or merge type
local _type = _identifier.definition.type.def
_type.name = modulename
-- if file (module) has no documentation add item documentation to it
-- else add it to the type.
if not file.description or file.description == "" then
file.description = _identifier.definition.description
else
_type.description = _identifier.definition.description
end
_identifier.definition.description = ""
if not file.shortdescription or file.shortdescription == "" then
file.shortdescription = _identifier.definition.shortdescription
else
_type.shortdescription = _identifier.definition.shortdescription
end
_identifier.definition.shortdescription = ""
-- WORKAROUND FOR BUG 421622: [outline]module selection in outline does not select it in texteditor
--_type.sourcerange.min = _identifier.definition.sourcerange.min
--_type.sourcerange.max = _identifier.definition.sourcerange.max
-- merge the type with priority to documentation except for source range
file:mergetype(_type,false,true)
-- create return if needed
if not file.returns[1] then
file.returns[1] = apimodel._return()
file.returns[1].types = { apimodel._internaltyperef(modulename) }
end
-- change the type of the identifier
_identifier.definition.type = apimodel._internaltyperef(modulename)
end
end
end
end
end
-- ----------------------------------------------------------
-- create the internalcontent from an ast metalua
-- ----------------------------------------------------------
function M.createinternalcontent (ast,file,comment2apiobj,modulename)
-- init cache
idto_block = {}
idto_identifier = {}
expreto_expression = {}
comment2apiobj = comment2apiobj or {}
file = file or apimodel._file()
-- execute code safely to be sure to clean cache correctly
local internalcontent
local ok, errmsg = pcall(function ()
-- add parent to all node
addparents(ast)
-- create block and expression node
internalcontent = createtreestructure(ast)
-- create Local vars, global vars and linked occurences (Items)
createvardefinitions(internalcontent,ast,file,comment2apiobj)
-- try to dectect module information from code
local moduletyperef = file:moduletyperef()
if moduletyperef and moduletyperef.tag == "internaltyperef" then
modulename = moduletyperef.typename or modulename
end
if modulename then
searchmodule(ast,file,comment2apiobj,modulename)
end
end)
-- clean cache
idto_block = {}
idto_identifier = {}
expreto_expression = {}
-- if not ok raise an error
if not ok then error (errmsg) end
return internalcontent
end
return M

View File

@@ -0,0 +1,656 @@
-------------------------------------------------------------------------------
-- Copyright (c) 2011-2013 Sierra Wireless and others.
-- All rights reserved. This program and the accompanying materials
-- are made available under the terms of the Eclipse Public License v1.0
-- which accompanies this distribution, and is available at
-- http://www.eclipse.org/legal/epl-v10.html
--
-- Contributors:
-- Sierra Wireless - initial API and implementation
-------------------------------------------------------------------------------
local mlc = require ('metalua.compiler').new()
local gg = require 'metalua.grammar.generator'
local lexer = require 'metalua.grammar.lexer'
local mlp = mlc.parser
local M = {} -- module
local lx -- lexer used to parse tag
local registeredparsers -- table {tagname => {list de parsers}}
-- ----------------------------------------------------
-- raise an error if result contains a node error
-- ----------------------------------------------------
local function raiserror(result)
for i, node in ipairs(result) do
assert(not node or node.tag ~= "Error")
end
end
-- ----------------------------------------------------
-- copy key and value from one table to an other
-- ----------------------------------------------------
local function copykey(tablefrom, tableto)
for key, value in pairs(tablefrom) do
if key ~= "lineinfos" then
tableto[key] = value
end
end
end
-- ----------------------------------------------------
-- Handle keyword and identifiers as word
-- ----------------------------------------------------
local function parseword(lx)
local word = lx :peek()
local tag = word.tag
if tag=='Keyword' or tag=='Id' then
lx:next()
return {tag='Word', lineinfo=word.lineinfo, word[1]}
else
return gg.parse_error(lx,'Id or Keyword expected')
end
end
-- ----------------------------------------------------
-- parse an id
-- return a table {name, lineinfo)
-- ----------------------------------------------------
local idparser = gg.sequence({
builder = function (result)
raiserror(result)
return { name = result[1][1] }
end,
parseword
})
-- ----------------------------------------------------
-- parse a modulename (id.)?id
-- return a table {name, lineinfo)
-- ----------------------------------------------------
local modulenameparser = gg.list({
builder = function (result)
raiserror(result)
local ids = {}
for i, id in ipairs(result) do
table.insert(ids,id.name)
end
return {name = table.concat(ids,".")}
end,
primary = idparser,
separators = '.'
})
-- ----------------------------------------------------
-- parse a typename (id.)?id
-- return a table {name, lineinfo)
-- ----------------------------------------------------
local typenameparser= modulenameparser
-- ----------------------------------------------------
-- parse an internaltype ref
-- return a table {name, lineinfo)
-- ----------------------------------------------------
local internaltyperefparser = gg.sequence({
builder = function(result)
raiserror(result)
return {tag = "typeref",type=result[1].name}
end,
"#", typenameparser
})
-- ----------------------------------------------------
-- parse en external type ref
-- return a table {name, lineinfo)
-- ----------------------------------------------------
local externaltyperefparser = gg.sequence({
builder = function(result)
raiserror(result)
return {tag = "typeref",module=result[1].name,type=result[2].name}
end,
modulenameparser,"#", typenameparser
})
-- ----------------------------------------------------
-- parse a typeref
-- return a table {name, lineinfo)
-- ----------------------------------------------------
local typerefparser = gg.multisequence{
internaltyperefparser,
externaltyperefparser}
-- ----------------------------------------------------
-- parse a list of typeref
-- return a list of table {name, lineinfo)
-- ----------------------------------------------------
local typereflistparser = gg.list({
primary = typerefparser,
separators = ','
})
-- ----------------------------------------------------
-- TODO use a more generic way to parse (modifier if not always a typeref)
-- TODO support more than one modifier
-- ----------------------------------------------------
local modifiersparser = gg.sequence({
builder = function(result)
raiserror(result)
return {[result[1].name]=result[2]}
end,
"[", idparser , "=" , internaltyperefparser , "]"
})
-- ----------------------------------------------------
-- parse a return tag
-- ----------------------------------------------------
local returnparsers = {
-- full parser
gg.sequence({
builder = function (result)
raiserror(result)
return { types= result[1]}
end,
'@','return', typereflistparser
}),
-- parser without typerefs
gg.sequence({
builder = function (result)
raiserror(result)
return { types = {}}
end,
'@','return'
})
}
-- ----------------------------------------------------
-- parse a param tag
-- ----------------------------------------------------
local paramparsers = {
-- full parser
gg.sequence({
builder = function (result)
raiserror(result)
return { name = result[2].name, type = result[1]}
end,
'@','param', typerefparser, idparser
}),
-- full parser without type
gg.sequence({
builder = function (result)
raiserror(result)
return { name = result[1].name}
end,
'@','param', idparser
}),
-- Parser for `Dots
gg.sequence({
builder = function (result)
raiserror(result)
return { name = '...' }
end,
'@','param', '...'
}),
}
-- ----------------------------------------------------
-- parse a field tag
-- ----------------------------------------------------
local fieldparsers = {
-- full parser
gg.sequence({
builder = function (result)
raiserror(result)
local tag = {}
copykey(result[1],tag)
tag.type = result[2]
tag.name = result[3].name
return tag
end,
'@','field', modifiersparser, typerefparser, idparser
}),
-- parser without name
gg.sequence({
builder = function (result)
raiserror(result)
local tag = {}
copykey(result[1],tag)
tag.type = result[2]
return tag
end,
'@','field', modifiersparser, typerefparser
}),
-- parser without type
gg.sequence({
builder = function (result)
raiserror(result)
local tag = {}
copykey(result[1],tag)
tag.name = result[2].name
return tag
end,
'@','field', modifiersparser, idparser
}),
-- parser without type and name
gg.sequence({
builder = function (result)
raiserror(result)
local tag = {}
copykey(result[1],tag)
return tag
end,
'@','field', modifiersparser
}),
-- parser without modifiers
gg.sequence({
builder = function (result)
raiserror(result)
return { name = result[2].name, type = result[1]}
end,
'@','field', typerefparser, idparser
}),
-- parser without modifiers and name
gg.sequence({
builder = function (result)
raiserror(result)
return {type = result[1]}
end,
'@','field', typerefparser
}),
-- parser without type and modifiers
gg.sequence({
builder = function (result)
raiserror(result)
return { name = result[1].name}
end,
'@','field', idparser
}),
-- parser with nothing
gg.sequence({
builder = function (result)
raiserror(result)
return {}
end,
'@','field'
})
}
-- ----------------------------------------------------
-- parse a function tag
-- TODO use a more generic way to parse modifier !
-- ----------------------------------------------------
local functionparsers = {
-- full parser
gg.sequence({
builder = function (result)
raiserror(result)
local tag = {}
copykey(result[1],tag)
tag.name = result[2].name
return tag
end,
'@','function', modifiersparser, idparser
}),
-- parser without name
gg.sequence({
builder = function (result)
raiserror(result)
local tag = {}
copykey(result[1],tag)
return tag
end,
'@','function', modifiersparser
}),
-- parser without modifier
gg.sequence({
builder = function (result)
raiserror(result)
local tag = {}
tag.name = result[1].name
return tag
end,
'@','function', idparser
}),
-- empty parser
gg.sequence({
builder = function (result)
raiserror(result)
return {}
end,
'@','function'
})
}
-- ----------------------------------------------------
-- parse a type tag
-- ----------------------------------------------------
local typeparsers = {
-- full parser
gg.sequence({
builder = function (result)
raiserror(result)
return { name = result[1].name}
end,
'@','type',typenameparser
}),
-- parser without name
gg.sequence({
builder = function (result)
raiserror(result)
return {}
end,
'@','type'
})
}
-- ----------------------------------------------------
-- parse a module tag
-- ----------------------------------------------------
local moduleparsers = {
-- full parser
gg.sequence({
builder = function (result)
raiserror(result)
return { name = result[1].name }
end,
'@','module', modulenameparser
}),
-- parser without name
gg.sequence({
builder = function (result)
raiserror(result)
return {}
end,
'@','module'
})
}
-- ----------------------------------------------------
-- parse a third tag
-- ----------------------------------------------------
local thirdtagsparser = gg.sequence({
builder = function (result)
raiserror(result)
return { name = result[1][1] }
end,
'@', mlp.id
})
-- ----------------------------------------------------
-- init parser
-- ----------------------------------------------------
local function initparser()
-- register parsers
-- each tag name has several parsers
registeredparsers = {
["module"] = moduleparsers,
["return"] = returnparsers,
["type"] = typeparsers,
["field"] = fieldparsers,
["function"] = functionparsers,
["param"] = paramparsers
}
-- create lexer used for parsing
lx = lexer.lexer:clone()
lx.extractors = {
-- "extract_long_comment",
-- "extract_short_comment",
-- "extract_long_string",
"extract_short_string",
"extract_word",
"extract_number",
"extract_symbol"
}
-- Add dots as keyword
local tagnames = { '...' }
-- Add tag names as key word
for tagname, _ in pairs(registeredparsers) do
table.insert(tagnames,tagname)
end
lx:add(tagnames)
return lx, parsers
end
initparser()
-- ----------------------------------------------------
-- get the string pattern to remove for each line of description
-- the goal is to fix the indentation problems
-- ----------------------------------------------------
local function getstringtoremove (stringcomment,commentstart)
local _,_,capture = string.find(stringcomment,"\n?([ \t]*)@[^{]+",commentstart)
if not capture then
_,_,capture = string.find(stringcomment,"^([ \t]*)",commentstart)
end
capture = string.gsub(capture,"(.)","%1?")
return capture
end
-- ----------------------------------------------------
-- parse comment tag partition and return table structure
-- ----------------------------------------------------
local function parsetag(part)
if part.comment:find("^@") then
-- check if the part start by a supported tag
for tagname,parsers in pairs(registeredparsers) do
if (part.comment:find("^@"..tagname)) then
-- try the registered parsers for this tag
local result
for i, parser in ipairs(parsers) do
local valid, tag = pcall(parser, lx:newstream(part.comment, tagname .. 'tag lexer'))
if valid then
-- add tagname
tag.tagname = tagname
-- add description
local endoffset = tag.lineinfo.last.offset
tag.description = part.comment:sub(endoffset+2,-1)
return tag
end
end
end
end
end
return nil
end
-- ----------------------------------------------------
-- Parse third party tags.
--
-- Enable to parse a tag not defined in language.
-- So for, accepted format is: @sometagname adescription
-- ----------------------------------------------------
local function parsethirdtag( part )
-- Check it there is someting to process
if not part.comment:find("^@") then
return nil, 'No tag to parse'
end
-- Apply parser
local status, parsedtag = pcall(thirdtagsparser, lx:newstream(part.comment, 'Third party tag lexer'))
if not status then
return nil, "Unable to parse given string."
end
-- Retrieve description
local endoffset = parsedtag.lineinfo.last.offset
local tag = {
description = part.comment:sub(endoffset+2,-1)
}
return parsedtag.name, tag
end
-- ---------------------------------------------------------
-- split string comment in several part
-- return list of {comment = string, offset = number}
-- the first part is the part before the first tag
-- the others are the part from a tag to the next one
-- ----------------------------------------------------
local function split(stringcomment,commentstart)
local partstart = commentstart
local result = {}
-- manage case where the comment start by @
-- (we must ignore the inline see tag @{..})
local at_startoffset, at_endoffset = stringcomment:find("^[ \t]*@[^{]",partstart)
if at_endoffset then
partstart = at_endoffset-1 -- we start before the @ and the non '{' character
end
-- split comment
-- (we must ignore the inline see tag @{..})
repeat
at_startoffset, at_endoffset = stringcomment:find("\n[ \t]*@[^{]",partstart)
local partend
if at_startoffset then
partend= at_startoffset-1 -- the end is before the separator pattern (just before the \n)
else
partend = #stringcomment -- we don't find any pattern so the end is the end of the string
end
table.insert(result, { comment = stringcomment:sub (partstart,partend) ,
offset = partstart})
if at_endoffset then
partstart = at_endoffset-1 -- the new start is befire the @ and the non { char
end
until not at_endoffset
return result
end
-- ----------------------------------------------------
-- parse a comment block and return a table
-- ----------------------------------------------------
function M.parse(stringcomment)
local _comment = {description="", shortdescription=""}
-- clean windows carriage return
stringcomment = string.gsub(stringcomment,"\r\n","\n")
-- check if it's a ld comment
-- get the begin of the comment
-- ============================
if not stringcomment:find("^-") then
-- if this comment don't start by -, we will not handle it.
return nil
end
-- retrieve the real start
local commentstart = 2 --after the first hyphen
-- if the first line is an empty comment line with at least 3 hyphens we ignore it
local _ , endoffset = stringcomment:find("^-+[ \t]*\n")
if endoffset then
commentstart = endoffset+1
end
-- clean comments
-- ===================
-- remove line of "-"
stringcomment = string.sub(stringcomment,commentstart)
-- clean indentation
local pattern = getstringtoremove (stringcomment,1)
stringcomment = string.gsub(stringcomment,"^"..pattern,"")
stringcomment = string.gsub(stringcomment,"\n"..pattern,"\n")
-- split comment part
-- ====================
local commentparts = split(stringcomment, 1)
-- Extract descriptions
-- ====================
local firstpart = commentparts[1].comment
if firstpart:find("^[^@]") or firstpart:find("^@{") then
-- if the comment part don't start by @
-- it's the part which contains descriptions
-- (there are an exception for the in-line see tag @{..})
local shortdescription, description = string.match(firstpart,'^(.-[.?])(%s.+)')
-- store description
if shortdescription then
_comment.shortdescription = shortdescription
-- clean description
-- remove always the first space character
-- (this manage the case short and long description is on the same line)
description = string.gsub(description, "^[ \t]","")
-- if first line is only an empty string remove it
description = string.gsub(description, "^[ \t]*\n","")
_comment.description = description
else
_comment.shortdescription = firstpart
_comment.description = ""
end
end
-- Extract tags
-- ===================
-- Parse regular tags
local tag
for i, part in ipairs(commentparts) do
tag = parsetag(part)
--if it's a supported tag (so tag is not nil, it's a table)
if tag then
if not _comment.tags then _comment.tags = {} end
if not _comment.tags[tag.tagname] then
_comment.tags[tag.tagname] = {}
end
table.insert(_comment.tags[tag.tagname], tag)
else
-- Try user defined tags, so far they will look like
-- @identifier description
local tagname, thirdtag = parsethirdtag( part )
if tagname then
--
-- Append found tag
--
local reservedname = 'unknowntags'
if not _comment.unknowntags then
_comment.unknowntags = {}
end
-- Create specific section for parsed tag
if not _comment.unknowntags[tagname] then
_comment.unknowntags[tagname] = {}
end
-- Append to specific section
table.insert(_comment.unknowntags[tagname], thirdtag)
end
end
end
return _comment
end
function M.parseinlinecomment(stringcomment)
--TODO this code is use to activate typage only on --- comments. (deactivate for now)
-- if not stringcomment or not stringcomment:find("^-") then
-- -- if this comment don't start by -, we will not handle it.
-- return nil
-- end
-- -- remove the first '-'
-- stringcomment = string.sub(stringcomment,2)
-- print (stringcomment)
-- io.flush()
local valid, parsedtag = pcall(typerefparser, lx:newstream(stringcomment, 'typeref parser'))
if valid then
local endoffset = parsedtag.lineinfo.last.offset
parsedtag.description = stringcomment:sub(endoffset+2,-1)
return parsedtag
end
end
return M