-------------------------------------------------------------------------------- -- 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 -- - 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