mirror of https://gitlab.com/ita1024/waf
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
212 lines
4.9 KiB
Python
212 lines
4.9 KiB
Python
#!/usr/bin/env python
|
|
# encoding: utf-8
|
|
# Thomas Nagy, 2016-2018 (ita)
|
|
|
|
"""
|
|
Provide a scanner for finding dependencies on d files
|
|
"""
|
|
|
|
import re
|
|
from waflib import Utils
|
|
|
|
def filter_comments(filename):
|
|
"""
|
|
:param filename: d file name
|
|
:type filename: string
|
|
:rtype: list
|
|
:return: a list of characters
|
|
"""
|
|
txt = Utils.readf(filename)
|
|
i = 0
|
|
buf = []
|
|
max = len(txt)
|
|
begin = 0
|
|
while i < max:
|
|
c = txt[i]
|
|
if c == '"' or c == "'": # skip a string or character literal
|
|
buf.append(txt[begin:i])
|
|
delim = c
|
|
i += 1
|
|
while i < max:
|
|
c = txt[i]
|
|
if c == delim:
|
|
break
|
|
elif c == '\\': # skip the character following backslash
|
|
i += 1
|
|
i += 1
|
|
i += 1
|
|
begin = i
|
|
elif c == '/': # try to replace a comment with whitespace
|
|
buf.append(txt[begin:i])
|
|
i += 1
|
|
if i == max:
|
|
break
|
|
c = txt[i]
|
|
if c == '+': # eat nesting /+ +/ comment
|
|
i += 1
|
|
nesting = 1
|
|
c = None
|
|
while i < max:
|
|
prev = c
|
|
c = txt[i]
|
|
if prev == '/' and c == '+':
|
|
nesting += 1
|
|
c = None
|
|
elif prev == '+' and c == '/':
|
|
nesting -= 1
|
|
if nesting == 0:
|
|
break
|
|
c = None
|
|
i += 1
|
|
elif c == '*': # eat /* */ comment
|
|
i += 1
|
|
c = None
|
|
while i < max:
|
|
prev = c
|
|
c = txt[i]
|
|
if prev == '*' and c == '/':
|
|
break
|
|
i += 1
|
|
elif c == '/': # eat // comment
|
|
i += 1
|
|
while i < max and txt[i] != '\n':
|
|
i += 1
|
|
else: # no comment
|
|
begin = i - 1
|
|
continue
|
|
i += 1
|
|
begin = i
|
|
buf.append(' ')
|
|
else:
|
|
i += 1
|
|
buf.append(txt[begin:])
|
|
return buf
|
|
|
|
class d_parser(object):
|
|
"""
|
|
Parser for d files
|
|
"""
|
|
def __init__(self, env, incpaths):
|
|
#self.code = ''
|
|
#self.module = ''
|
|
#self.imports = []
|
|
|
|
self.allnames = []
|
|
|
|
self.re_module = re.compile(r"module\s+([^;]+)")
|
|
self.re_import = re.compile(r"import\s+([^;]+)")
|
|
self.re_import_bindings = re.compile("([^:]+):(.*)")
|
|
self.re_import_alias = re.compile("[^=]+=(.+)")
|
|
|
|
self.env = env
|
|
|
|
self.nodes = []
|
|
self.names = []
|
|
|
|
self.incpaths = incpaths
|
|
|
|
def tryfind(self, filename):
|
|
"""
|
|
Search file a file matching an module/import directive
|
|
|
|
:param filename: file to read
|
|
:type filename: string
|
|
"""
|
|
found = 0
|
|
for n in self.incpaths:
|
|
found = n.find_resource(filename.replace('.', '/') + '.d')
|
|
if found:
|
|
self.nodes.append(found)
|
|
self.waiting.append(found)
|
|
break
|
|
if not found:
|
|
if not filename in self.names:
|
|
self.names.append(filename)
|
|
|
|
def get_strings(self, code):
|
|
"""
|
|
:param code: d code to parse
|
|
:type code: string
|
|
:return: the modules that the code uses
|
|
:rtype: a list of match objects
|
|
"""
|
|
#self.imports = []
|
|
self.module = ''
|
|
lst = []
|
|
|
|
# get the module name (if present)
|
|
|
|
mod_name = self.re_module.search(code)
|
|
if mod_name:
|
|
self.module = re.sub(r'\s+', '', mod_name.group(1)) # strip all whitespaces
|
|
|
|
# go through the code, have a look at all import occurrences
|
|
|
|
# first, lets look at anything beginning with "import" and ending with ";"
|
|
import_iterator = self.re_import.finditer(code)
|
|
if import_iterator:
|
|
for import_match in import_iterator:
|
|
import_match_str = re.sub(r'\s+', '', import_match.group(1)) # strip all whitespaces
|
|
|
|
# does this end with an import bindings declaration?
|
|
# (import bindings always terminate the list of imports)
|
|
bindings_match = self.re_import_bindings.match(import_match_str)
|
|
if bindings_match:
|
|
import_match_str = bindings_match.group(1)
|
|
# if so, extract the part before the ":" (since the module declaration(s) is/are located there)
|
|
|
|
# split the matching string into a bunch of strings, separated by a comma
|
|
matches = import_match_str.split(',')
|
|
|
|
for match in matches:
|
|
alias_match = self.re_import_alias.match(match)
|
|
if alias_match:
|
|
# is this an alias declaration? (alias = module name) if so, extract the module name
|
|
match = alias_match.group(1)
|
|
|
|
lst.append(match)
|
|
return lst
|
|
|
|
def start(self, node):
|
|
"""
|
|
The parsing starts here
|
|
|
|
:param node: input file
|
|
:type node: :py:class:`waflib.Node.Node`
|
|
"""
|
|
self.waiting = [node]
|
|
# while the stack is not empty, add the dependencies
|
|
while self.waiting:
|
|
nd = self.waiting.pop(0)
|
|
self.iter(nd)
|
|
|
|
def iter(self, node):
|
|
"""
|
|
Find all the modules that a file depends on, uses :py:meth:`waflib.Tools.d_scan.d_parser.tryfind` to process dependent files
|
|
|
|
:param node: input file
|
|
:type node: :py:class:`waflib.Node.Node`
|
|
"""
|
|
path = node.abspath() # obtain the absolute path
|
|
code = "".join(filter_comments(path)) # read the file and filter the comments
|
|
names = self.get_strings(code) # obtain the import strings
|
|
for x in names:
|
|
# optimization
|
|
if x in self.allnames:
|
|
continue
|
|
self.allnames.append(x)
|
|
|
|
# for each name, see if it is like a node or not
|
|
self.tryfind(x)
|
|
|
|
def scan(self):
|
|
"look for .d/.di used by a d file"
|
|
env = self.env
|
|
gruik = d_parser(env, self.generator.includes_nodes)
|
|
node = self.inputs[0]
|
|
gruik.start(node)
|
|
nodes = gruik.nodes
|
|
names = gruik.names
|
|
return (nodes, names)
|
|
|