-------------------------------------------------------------------------------- | |
-- 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, rightpart } -> | |
if not expr.lineinfo then return nil end | |
-- create index | |
local _expression = createexpr(innerexpr,_block) | |
if _expression then | |
if rightpart and rightpart.tag=='String' then | |
_expr = internalmodel._index(_expression,rightpart[1]) | |
else | |
_expr = internalmodel._index(_expression,nil) | |
end | |
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("table") | |
-- 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 |