jack2 codebase
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

210 lines
5.0KB

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