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