1 -------------------------------------------------------------------------------
2 -- Copyright (c) 2006-2013 Fabien Fleutot and others.
4 -- All rights reserved.
6 -- This program and the accompanying materials are made available
7 -- under the terms of the Eclipse Public License v1.0 which
8 -- accompanies this distribution, and is available at
9 -- http://www.eclipse.org/legal/epl-v10.html
11 -- This program and the accompanying materials are also made available
12 -- under the terms of the MIT public license which accompanies this
13 -- distribution, and is available at http://www.lua.org/license.html
16 -- Fabien Fleutot - API and implementation
18 -------------------------------------------------------------------------------
20 local walk = require 'metalua.treequery.walk'
23 -- support for old-style modules
26 -- -----------------------------------------------------------------------------
27 -- -----------------------------------------------------------------------------
29 -- multimap helper mmap: associate a key to a set of values
31 -- -----------------------------------------------------------------------------
32 -- -----------------------------------------------------------------------------
34 local function mmap_add (mmap, node, x)
35 if node==nil then return false end
36 local set = mmap[node]
37 if set then set[x] = true
38 else mmap[node] = {[x]=true} end
41 -- currently unused, I throw the whole set away
42 local function mmap_remove (mmap, node, x)
43 local set = mmap[node]
44 if not set then return false
45 elseif not set[x] then return false
46 elseif next(set) then set[x]=nil
47 else mmap[node] = nil end
51 -- -----------------------------------------------------------------------------
52 -- -----------------------------------------------------------------------------
56 -- -----------------------------------------------------------------------------
57 -- -----------------------------------------------------------------------------
59 local ACTIVE_SCOPE = setmetatable({ }, {__mode="k"})
61 -- treequery metatable
62 local Q = { }; Q.__index = Q
64 --- treequery constructor
65 -- the resultingg object will allow to filter ans operate on the AST
66 -- @param root the AST to visit
67 -- @return a treequery visitor instance
68 function M.treequery(root)
81 -- helper to share the implementations of positional filters
82 local function add_pos_filter(self, position, inverted, inclusive, f, ...)
83 if type(f)=='string' then f = M.has_tag(f, ...) end
84 if not inverted then self.unsatisfied += 1 end
89 inverted = inverted or false,
90 inclusive = inclusive or false }
91 table.insert(self.predicates, x)
95 function Q :if_unknown(f)
96 self.unknown_handler = f or (||nil)
100 -- TODO: offer an API for inclusive pos_filters
102 --- select nodes which are after one which satisfies predicate f
103 Q.after = |self, f, ...| add_pos_filter(self, 'after', false, false, f, ...)
104 --- select nodes which are not after one which satisfies predicate f
105 Q.not_after = |self, f, ...| add_pos_filter(self, 'after', true, false, f, ...)
106 --- select nodes which are under one which satisfies predicate f
107 Q.under = |self, f, ...| add_pos_filter(self, 'under', false, false, f, ...)
108 --- select nodes which are not under one which satisfies predicate f
109 Q.not_under = |self, f, ...| add_pos_filter(self, 'under', true, false, f, ...)
111 --- select nodes which satisfy predicate f
112 function Q :filter(f, ...)
113 if type(f)=='string' then f = M.has_tag(f, ...) end
114 table.insert(self.filters, f);
118 --- select nodes which satisfy predicate f
119 function Q :filter_not(f, ...)
120 if type(f)=='string' then f = M.has_tag(f, ...) end
121 table.insert(self.filters, |...| not f(...))
125 -- private helper: apply filters and execute up/down callbacks when applicable
126 function Q :execute()
128 -- TODO: optimize away not_under & not_after by pruning the tree
129 function cfg.down(...)
130 --printf ("[down]\t%s\t%s", self.unsatisfied, table.tostring((...)))
131 ACTIVE_SCOPE[...] = cfg.scope
132 local satisfied = self.unsatisfied==0
133 for _, x in ipairs(self.predicates) do
134 if not x.satisfied and x.pred(...) then
136 local node, parent = ...
137 local inc = x.inverted and 1 or -1
138 if x.position=='under' then
139 -- satisfied from after we get down this node...
140 self.unsatisfied += inc
141 -- ...until before we get up this node
142 mmap_add(self.until_up, node, x)
143 elseif x.position=='after' then
144 -- satisfied from after we get up this node...
145 mmap_add(self.from_up, node, x)
146 -- ...until before we get up this node's parent
147 mmap_add(self.until_up, parent, x)
148 elseif x.position=='under_or_after' then
149 -- satisfied from after we get down this node...
150 self.satisfied += inc
151 -- ...until before we get up this node's parent...
152 mmap_add(self.until_up, parent, x)
154 error "position not understood"
156 if x.inclusive then satisfied = self.unsatisfied==0 end
157 end -- predicate passed
158 end -- for predicates
161 for _, f in ipairs(self.filters) do
162 if not f(...) then satisfied=false; break end
164 if satisfied and self.down_f then self.down_f(...) end
169 --printf ("[up]\t%s", table.tostring((...)))
171 -- Remove predicates which are due before we go up this node
172 local preds = self.until_up[...]
174 for x, _ in pairs(preds) do
175 local inc = x.inverted and -1 or 1
176 self.unsatisfied += inc
179 self.until_up[...] = nil
182 -- Execute the up callback
183 -- TODO: cache the filter passing result from the down callback
184 -- TODO: skip if there's no callback
185 local satisfied = self.unsatisfied==0
187 for _, f in ipairs(self.filters) do
188 if not f(self, ...) then satisfied=false; break end
190 if satisfied and self.up_f then self.up_f(...) end
193 -- Set predicate which are due after we go up this node
194 local preds = self.from_up[...]
196 for p, _ in pairs(preds) do
197 local inc = p.inverted and 1 or -1
198 self.unsatisfied += inc
200 self.from_up[...] = nil
202 ACTIVE_SCOPE[...] = nil
205 function cfg.binder(id_node, ...)
206 --printf(" >>> Binder called on %s, %s", table.tostring(id_node),
207 -- table.tostring{...}:sub(2,-2))
208 cfg.down(id_node, ...)
210 --printf("down/up on binder done")
213 cfg.unknown = self.unknown_handler
215 --function cfg.occurrence (binder, occ)
216 -- if binder then OCC2BIND[occ] = binder[1] end
217 --printf(" >>> %s is an occurrence of %s", occ[1], table.tostring(binder and binder[2]))
220 --function cfg.binder(...) cfg.down(...); cfg.up(...) end
221 return walk.guess(cfg, self.root)
224 --- Execute a function on each selected node
225 -- @down: function executed when we go down a node, i.e. before its children
226 -- have been examined.
227 -- @up: function executed when we go up a node, i.e. after its children
228 -- have been examined.
229 function Q :foreach(down, up)
230 if not up and not down then
231 error "iterator missing"
235 return self :execute()
238 --- Return the list of nodes selected by a given treequery.
241 self :foreach(|x| table.insert(acc, x))
245 --- Return the first matching element
246 -- TODO: dirty hack, to implement properly with a 'break' return.
247 -- Also, it won't behave correctly if a predicate causes an error,
248 -- or if coroutines are involved.
251 local function f(...) result = {...}; error() end
252 pcall(|| self :foreach(f))
253 return unpack(result)
256 --- Pretty printer for queries
257 function Q :__tostring() return "<treequery>" end
259 -- -----------------------------------------------------------------------------
260 -- -----------------------------------------------------------------------------
264 -- -----------------------------------------------------------------------------
265 -- -----------------------------------------------------------------------------
267 --- Return a predicate which is true if the tested node's tag is among the
268 -- one listed as arguments
269 -- @param ... a sequence of tag names
270 function M.has_tag(...)
274 return (|node| node.tag==tag)
275 --return function(self, node) printf("node %s has_tag %s?", table.tostring(node), tag); return node.tag==tag end
278 for _, tag in ipairs(args) do tags[tag]=true end
279 return function(node)
280 local node_tag = node.tag
281 return node_tag and tags[node_tag]
286 --- Predicate to test whether a node represents an expression.
287 M.is_expr = M.has_tag('Nil', 'Dots', 'True', 'False', 'Number','String',
288 'Function', 'Table', 'Op', 'Paren', 'Call', 'Invoke',
291 -- helper for is_stat
292 local STAT_TAGS = { Do=1, Set=1, While=1, Repeat=1, If=1, Fornum=1,
293 Forin=1, Local=1, Localrec=1, Return=1, Break=1 }
295 --- Predicate to test whether a node represents a statement.
296 -- It is context-aware, i.e. it recognizes `Call and `Invoke nodes
297 -- used in a statement context as such.
298 function M.is_stat(node, parent)
300 if not tag then return false
301 elseif STAT_TAGS[tag] then return true
302 elseif tag=='Call' or tag=='Invoke' then return parent.tag==nil
303 else return false end
306 --- Predicate to test whether a node represents a statements block.
307 function M.is_block(node) return node.tag==nil end
309 -- -----------------------------------------------------------------------------
310 -- -----------------------------------------------------------------------------
312 -- Variables and scopes.
314 -- -----------------------------------------------------------------------------
315 -- -----------------------------------------------------------------------------
317 local BINDER_PARENT_TAG = {
318 Local=true, Localrec=true, Forin=true, Function=true }
320 --- Test whether a node is a binder. This is local predicate, although it
321 -- might need to inspect the parent node.
322 function M.is_binder(node, parent)
323 --printf('is_binder(%s, %s)', table.tostring(node), table.tostring(parent))
324 if node.tag ~= 'Id' or not parent then return false end
325 if parent.tag=='Fornum' then return parent[1]==node end
326 if not BINDER_PARENT_TAG[parent.tag] then return false end
327 for _, binder in ipairs(parent[1]) do
328 if binder==node then return true end
333 --- Retrieve the binder associated to an occurrence within root.
334 -- @param occurrence an Id node representing an occurrence in `root`.
335 -- @param root the tree in which `node` and its binder occur.
336 -- @return the binder node, and its ancestors up to root if found.
337 -- @return nil if node is global (or not an occurrence) in `root`.
338 function M.binder(occurrence, root)
339 local cfg, id_name, result = { }, occurrence[1], { }
340 function cfg.occurrence(id)
341 if id == occurrence then result = cfg.scope :get(id_name) end
342 -- TODO: break the walker
344 walk.guess(cfg, root)
345 return unpack(result)
348 --- Predicate to filter occurrences of a given binder.
349 -- Warning: it relies on internal scope book-keeping,
350 -- and for this reason, it only works as query method argument.
351 -- It won't work outside of a query.
352 -- @param binder the binder whose occurrences must be kept by predicate
353 -- @return a predicate
355 -- function M.is_occurrence_of(binder)
356 -- return function(node, ...)
357 -- if node.tag ~= 'Id' then return nil end
358 -- if M.is_binder(node, ...) then return nil end
359 -- local scope = ACTIVE_SCOPE[node]
360 -- if not scope then return nil end
361 -- local result = scope :get (node[1]) or { }
362 -- if result[1] ~= binder then return nil end
363 -- return unpack(result)
367 function M.is_occurrence_of(binder)
368 return function(node, ...)
369 local b = M.get_binder(node)
370 return b and b==binder
374 function M.get_binder(occurrence, ...)
375 if occurrence.tag ~= 'Id' then return nil end
376 if M.is_binder(occurrence, ...) then return nil end
377 local scope = ACTIVE_SCOPE[occurrence]
378 local binder_hierarchy = scope :get(occurrence[1])
379 return unpack (binder_hierarchy or { })
382 --- Transform a predicate on a node into a predicate on this node's
383 -- parent. For instance if p tests whether a node has property P,
384 -- then parent(p) tests whether this node's parent has property P.
385 -- The ancestor level is precised with n, with 1 being the node itself,
386 -- 2 its parent, 3 its grand-parent etc.
387 -- @param[optional] n the parent to examine, default=2
388 -- @param pred the predicate to transform
389 -- @return a predicate
390 function M.parent(n, pred, ...)
391 if type(n)~='number' then n, pred = 2, n end
392 if type(pred)=='string' then pred = M.has_tag(pred, ...) end
393 return function(self, ...)
394 return select(n, ...) and pred(self, select(n, ...))
398 --- Transform a predicate on a node into a predicate on this node's
400 -- @param n the child's index number
401 -- @param pred the predicate to transform
402 -- @return a predicate
403 function M.child(n, pred)
404 return function(node, ...)
405 local child = node[n]
406 return child and pred(child, node, ...)
410 --- Predicate to test the position of a node in its parent.
411 -- The predicate succeeds if the node is the n-th child of its parent,
413 -- nth(a) is equivalent to nth(a, a).
414 -- Negative indices are admitted, and count from the last child,
415 -- as done for instance by string.sub().
417 -- TODO: This is wrong, this tests the table relationship rather than the
418 -- AST node relationship.
419 -- Must build a getindex helper, based on pattern matching, then build
420 -- the predicate around it.
422 -- @param a lower bound
423 -- @param a upper bound
424 -- @return a predicate
425 function M.is_nth(a, b)
427 return function(self, node, parent)
428 if not parent then return false end
429 local nchildren = #parent
430 local a = a<=0 and nchildren+a+1 or a
431 if a>nchildren then return false end
432 local b = b<=0 and nchildren+b+1 or b>nchildren and nchildren or b
433 for i=a,b do if parent[i]==node then return true end end
439 -- -----------------------------------------------------------------------------
440 -- -----------------------------------------------------------------------------
444 -- -----------------------------------------------------------------------------
445 -- -----------------------------------------------------------------------------
447 local comment_extractor = |which_side| function (node)
448 local x = node.lineinfo
449 x = x and x[which_side]
451 if not x then return nil end
453 for _, record in ipairs(x) do
454 table.insert(lines, record[1])
456 return table.concat(lines, '\n')
459 M.comment_prefix = comment_extractor 'first'
460 M.comment_suffix = comment_extractor 'last'
463 --- Shortcut for the query constructor
464 function M :__call(...) return self.treequery(...) end