d_scan.py 4.9 KB

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