1#!/usr/bin/env python
2# encoding: utf-8
3# Thomas Nagy, 2016-2018 (ita)
4
5"""
6Provide a scanner for finding dependencies on d files
7"""
8
9import re
10from waflib import Utils
11
12def filter_comments(filename):
13	"""
14	:param filename: d file name
15	:type filename: string
16	:rtype: list
17	:return: a list of characters
18	"""
19	txt = Utils.readf(filename)
20	i = 0
21	buf = []
22	max = len(txt)
23	begin = 0
24	while i < max:
25		c = txt[i]
26		if c == '"' or c == "'":  # skip a string or character literal
27			buf.append(txt[begin:i])
28			delim = c
29			i += 1
30			while i < max:
31				c = txt[i]
32				if c == delim:
33					break
34				elif c == '\\':  # skip the character following backslash
35					i += 1
36				i += 1
37			i += 1
38			begin = i
39		elif c == '/':  # try to replace a comment with whitespace
40			buf.append(txt[begin:i])
41			i += 1
42			if i == max:
43				break
44			c = txt[i]
45			if c == '+':  # eat nesting /+ +/ comment
46				i += 1
47				nesting = 1
48				c = None
49				while i < max:
50					prev = c
51					c = txt[i]
52					if prev == '/' and c == '+':
53						nesting += 1
54						c = None
55					elif prev == '+' and c == '/':
56						nesting -= 1
57						if nesting == 0:
58							break
59						c = None
60					i += 1
61			elif c == '*':  # eat /* */ comment
62				i += 1
63				c = None
64				while i < max:
65					prev = c
66					c = txt[i]
67					if prev == '*' and c == '/':
68						break
69					i += 1
70			elif c == '/':  # eat // comment
71				i += 1
72				while i < max and txt[i] != '\n':
73					i += 1
74			else:  # no comment
75				begin = i - 1
76				continue
77			i += 1
78			begin = i
79			buf.append(' ')
80		else:
81			i += 1
82	buf.append(txt[begin:])
83	return buf
84
85class d_parser(object):
86	"""
87	Parser for d files
88	"""
89	def __init__(self, env, incpaths):
90		#self.code = ''
91		#self.module = ''
92		#self.imports = []
93
94		self.allnames = []
95
96		self.re_module = re.compile(r"module\s+([^;]+)")
97		self.re_import = re.compile(r"import\s+([^;]+)")
98		self.re_import_bindings = re.compile("([^:]+):(.*)")
99		self.re_import_alias = re.compile("[^=]+=(.+)")
100
101		self.env = env
102
103		self.nodes = []
104		self.names = []
105
106		self.incpaths = incpaths
107
108	def tryfind(self, filename):
109		"""
110		Search file a file matching an module/import directive
111
112		:param filename: file to read
113		:type filename: string
114		"""
115		found = 0
116		for n in self.incpaths:
117			found = n.find_resource(filename.replace('.', '/') + '.d')
118			if found:
119				self.nodes.append(found)
120				self.waiting.append(found)
121				break
122		if not found:
123			if not filename in self.names:
124				self.names.append(filename)
125
126	def get_strings(self, code):
127		"""
128		:param code: d code to parse
129		:type code: string
130		:return: the modules that the code uses
131		:rtype: a list of match objects
132		"""
133		#self.imports = []
134		self.module = ''
135		lst = []
136
137		# get the module name (if present)
138
139		mod_name = self.re_module.search(code)
140		if mod_name:
141			self.module = re.sub(r'\s+', '', mod_name.group(1)) # strip all whitespaces
142
143		# go through the code, have a look at all import occurrences
144
145		# first, lets look at anything beginning with "import" and ending with ";"
146		import_iterator = self.re_import.finditer(code)
147		if import_iterator:
148			for import_match in import_iterator:
149				import_match_str = re.sub(r'\s+', '', import_match.group(1)) # strip all whitespaces
150
151				# does this end with an import bindings declaration?
152				# (import bindings always terminate the list of imports)
153				bindings_match = self.re_import_bindings.match(import_match_str)
154				if bindings_match:
155					import_match_str = bindings_match.group(1)
156					# if so, extract the part before the ":" (since the module declaration(s) is/are located there)
157
158				# split the matching string into a bunch of strings, separated by a comma
159				matches = import_match_str.split(',')
160
161				for match in matches:
162					alias_match = self.re_import_alias.match(match)
163					if alias_match:
164						# is this an alias declaration? (alias = module name) if so, extract the module name
165						match = alias_match.group(1)
166
167					lst.append(match)
168		return lst
169
170	def start(self, node):
171		"""
172		The parsing starts here
173
174		:param node: input file
175		:type node: :py:class:`waflib.Node.Node`
176		"""
177		self.waiting = [node]
178		# while the stack is not empty, add the dependencies
179		while self.waiting:
180			nd = self.waiting.pop(0)
181			self.iter(nd)
182
183	def iter(self, node):
184		"""
185		Find all the modules that a file depends on, uses :py:meth:`waflib.Tools.d_scan.d_parser.tryfind` to process dependent files
186
187		:param node: input file
188		:type node: :py:class:`waflib.Node.Node`
189		"""
190		path = node.abspath() # obtain the absolute path
191		code = "".join(filter_comments(path)) # read the file and filter the comments
192		names = self.get_strings(code) # obtain the import strings
193		for x in names:
194			# optimization
195			if x in self.allnames:
196				continue
197			self.allnames.append(x)
198
199			# for each name, see if it is like a node or not
200			self.tryfind(x)
201
202def scan(self):
203	"look for .d/.di used by a d file"
204	env = self.env
205	gruik = d_parser(env, self.generator.includes_nodes)
206	node = self.inputs[0]
207	gruik.start(node)
208	nodes = gruik.nodes
209	names = gruik.names
210	return (nodes, names)
211
212