1#************************************************************** 2# 3# Licensed to the Apache Software Foundation (ASF) under one 4# or more contributor license agreements. See the NOTICE file 5# distributed with this work for additional information 6# regarding copyright ownership. The ASF licenses this file 7# to you under the Apache License, Version 2.0 (the 8# "License"); you may not use this file except in compliance 9# with the License. You may obtain a copy of the License at 10# 11# http://www.apache.org/licenses/LICENSE-2.0 12# 13# Unless required by applicable law or agreed to in writing, 14# software distributed under the License is distributed on an 15# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16# KIND, either express or implied. See the License for the 17# specific language governing permissions and limitations 18# under the License. 19# 20#************************************************************** 21 22"""Statically extract the (namespace, local name) pairs that the ODF export 23code can emit. Usage: python3 -I cov_source.py <repo-main-dir> <out-dir> 24 25Writes emit.tsv, emit-unresolved.tsv and emit-skipped.tsv into <out-dir>. 26""" 27 28import collections 29import os 30import re 31import sys 32 33SRC_EXT = (".cxx", ".hxx", ".h") 34SKIP_DIRS = {"test", "qa", "workben", "unotest", "transform", "unoxml", "solver"} 35SKIP_PATHS = ("filter/source/svg/",) 36KEEP_NAMES = {"impastp4.cxx"} 37IMPORT_NAME = re.compile(r"import|imp", re.I) 38 39NS_RE = re.compile(r"XML_NAMESPACE_([A-Z][A-Z0-9_]*)") 40TOK_RE = re.compile(r"\bXML_[A-Z_][A-Z0-9_]*\b") 41STR_RE = re.compile(r'"((?:[^"\\\n]|\\.)*)"') 42NO_EXPORT = re.compile(r"\bMID_FLAG_NO_PROPERTY(_EXPORT)?\b|\bMID_SW_FLAG_NO_ITEM(_EXPORT)?\b") 43ELEMENT_ROW = re.compile(r"\bMID_(SW_)?FLAG_ELEMENT_ITEM(_EXPORT)?\b") 44PROP_ROW = re.compile(r"\bMID_\w+|\bXML_(?:\w+_)?TYPE_|\bsizeof\s*\(") 45EVENT_ROW = re.compile(r'^\{\s*"[^"]*"\s*,\s*XML_NAMESPACE_\w+\s*,\s*"') 46EMIT_ATTR = ("AddAttribute", "AddAttributeASCII") 47EMIT_FUNCS = set(EMIT_ATTR) | {"AddAttributeIdLegacy", "SvXMLElementExport", "GetQNameByKey"} 48DECL_ARG = re.compile(r"^(?:const\s+|enum\s+|sal_uInt16\b|SvXMLExport\b|SvXMLAttributeList\b)") 49 50 51def read(path): 52 with open(path, errors="replace") as f: 53 return f.read() 54 55 56def mask(text): 57 """Blank comments and string contents (same length, newlines kept).""" 58 out = list(text) 59 i, n = 0, len(text) 60 while i < n: 61 c = text[i] 62 if c == "/" and text[i:i + 2] == "//": 63 j = text.find("\n", i) 64 j = n if j < 0 else j 65 for k in range(i, j): 66 out[k] = " " 67 i = j 68 elif c == "/" and text[i:i + 2] == "/*": 69 j = text.find("*/", i + 2) 70 j = n if j < 0 else j + 2 71 for k in range(i, j): 72 if text[k] != "\n": 73 out[k] = " " 74 i = j 75 elif c in "\"'": 76 j = i + 1 77 while j < n and text[j] != c and text[j] != "\n": 78 j += 2 if text[j] == "\\" else 1 79 for k in range(i + 1, min(j, n)): 80 out[k] = " " 81 i = j + 1 82 else: 83 i += 1 84 return "".join(out) 85 86 87def directive_spans(text): 88 """Character ranges of preprocessor lines, with backslash continuations.""" 89 spans = [] 90 for m in re.finditer(r"^[ \t]*#(?:[^\n]*\\\n)*[^\n]*", text, re.M): 91 spans.append((m.start(), m.end())) 92 return spans 93 94 95def blank(masked, spans): 96 out = list(masked) 97 for a, b in spans: 98 for k in range(a, b): 99 if out[k] != "\n": 100 out[k] = " " 101 return "".join(out) 102 103 104def match_close(m, i): 105 """Index of the bracket closing the one at m[i].""" 106 depth = 0 107 for k in range(i, len(m)): 108 c = m[k] 109 if c in "({[": 110 depth += 1 111 elif c in ")}]": 112 depth -= 1 113 if depth == 0: 114 return k 115 return len(m) - 1 116 117 118def split_args(m, open_i, close_i): 119 """(start, end) of each top-level argument between two brackets.""" 120 args, depth, start = [], 0, open_i + 1 121 for k in range(open_i + 1, close_i): 122 c = m[k] 123 if c in "({[": 124 depth += 1 125 elif c in ")}]": 126 depth -= 1 127 elif c == "," and depth == 0: 128 args.append((start, k)) 129 start = k + 1 130 args.append((start, close_i)) 131 return args 132 133 134def enclosing_call(m, pos): 135 """(function name, argument index, argument count) of the innermost call around pos.""" 136 depth = 0 137 for k in range(pos - 1, -1, -1): 138 c = m[k] 139 if c in ")}]": 140 depth += 1 141 elif c in "({[": 142 if depth == 0: 143 if c != "(": 144 return None 145 name = re.search(r"(\w+)\s*$", m[max(0, k - 60):k]) 146 a = split_args(m, k, match_close(m, k)) 147 idx = next((n for n, (x, y) in enumerate(a) if x <= pos < y), None) 148 return (name.group(1) if name else None), idx, len(a) 149 depth -= 1 150 return None 151 152 153def parse_tokens(path): 154 toks = {} 155 pat = re.compile(r'TOKEN\(\s*"((?:[^"\\]|\\.)*)"\s*,\s*(XML_\w+)\s*(?:/\*.*?\*/)?\s*\)') 156 for line in read(path).splitlines(): 157 if line.lstrip().startswith("//"): 158 continue 159 m = pat.search(line) 160 if m: 161 toks[m.group(2)] = m.group(1).replace('\\"', '"') 162 return toks 163 164 165ADD_RE = re.compile(r"(?:\b_GetNamespaceMap\s*\(\s*\)\s*\.|\bmpNamespaceMap\s*->)\s*Add(?:AtIndex)?\s*\(") 166 167 168def ns_adds(masked, clean, toks): 169 """(key, URI) pairs from export-side namespace map Add/AddAtIndex calls.""" 170 defs = dict(re.findall(r'^[ \t]*#[ \t]*define[ \t]+(\w+)[ \t]+"([^"]*)"', clean, re.M)) 171 out = [] 172 for m in ADD_RE.finditer(masked): 173 o = m.end() - 1 174 a = split_args(masked, o, match_close(masked, o)) 175 if len(a) not in (3, 4): 176 continue 177 key = NS_RE.fullmatch(masked[a[-1][0]:a[-1][1]].strip()) 178 arg = clean[a[-2][0]:a[-2][1]] 179 tok = re.search(r"\b(XML_N_\w+)", arg) 180 lit = next((defs[w] for w in re.findall(r"\w+", arg) if w in defs), None) 181 uri = toks.get(tok.group(1)) if tok else lit 182 if key and uri is not None: 183 out.append((key.group(1), uri)) 184 return out 185 186 187class Macro: 188 def __init__(self, ns_i, ns_fixed, tok_i, tok_prefix): 189 self.ns_i, self.ns_fixed, self.tok_i, self.tok_prefix = ns_i, ns_fixed, tok_i, tok_prefix 190 191 192def parse_macros(text): 193 """Function-like macros that carry a namespace key and a token parameter.""" 194 macros = {} 195 for m in re.finditer(r"^[ \t]*#[ \t]*define[ \t]+(\w+)\(([^)]*)\)((?:[^\n]|\\\n)*)", text, re.M): 196 name, params, body = m.group(1), [p.strip() for p in m.group(2).split(",")], m.group(3) 197 ns = re.search(r"XML_NAMESPACE_##(\w+)", body) 198 fixed = re.search(r"XML_NAMESPACE_([A-Z][A-Z0-9_]*)\s*,\s*(\w+)", body) 199 if ns and ns.group(1) in params: 200 ns_i = params.index(ns.group(1)) 201 tok = re.search(r"(?:xmloff::token::)?XML_##(\w+)", body) 202 if tok and tok.group(1) in params: 203 macros[name] = Macro(ns_i, None, params.index(tok.group(1)), "XML_") 204 continue 205 tok = re.search(r"xmloff::token::(\w+)", body) 206 if tok and tok.group(1) in params: 207 macros[name] = Macro(ns_i, None, params.index(tok.group(1)), "") 208 elif fixed and fixed.group(2) in params: 209 macros[name] = Macro(None, fixed.group(1), params.index(fixed.group(2)), "") 210 return macros 211 212 213def included(text): 214 return re.findall(r'^[ \t]*#[ \t]*include[ \t]*["<]([^">]+)[">]', text, re.M) 215 216 217KEYWORDS = {"if", "for", "while", "switch", "catch", "return", "sizeof", "else"} 218DEF_TAIL = re.compile(r"\s*(?:const\s*)?(?:throw\s*\([^)]*\)\s*)?(?::[^{;]*)?\{") 219 220 221def func_defs(m): 222 """(name, parameter identifiers, body start, body end) of function definitions.""" 223 out = [] 224 for mm in re.finditer(r"\b([A-Za-z_]\w*)\s*\(", m): 225 if mm.group(1) in KEYWORDS: 226 continue 227 o = mm.end() - 1 228 c = match_close(m, o) 229 tail = DEF_TAIL.match(m, c + 1) 230 if not tail: 231 continue 232 params = [] 233 for a, b in split_args(m, o, c): 234 w = re.findall(r"\w+", re.sub(r"=.*", "", m[a:b], flags=re.S)) 235 params.append(w[-1] if len(w) > 1 else None) 236 b0 = tail.end() - 1 237 out.append((mm.group(1), params, b0, match_close(m, b0))) 238 return out 239 240 241class Wrapper: 242 def __init__(self, tok_i, nparams, keys, ns_param, role): 243 self.tok_i, self.nparams, self.keys, self.ns_param, self.role = tok_i, nparams, keys, ns_param, role 244 245 246class Scanner: 247 def __init__(self, main): 248 self.main = main 249 self.toks = parse_tokens(os.path.join(main, "xmloff/source/core/xmltoken.cxx")) 250 self.pairs = collections.defaultdict(lambda: {"roles": set(), "mods": set(), "ex": []}) 251 self.unres, self.skipped, self.files = [], [], [] 252 self.header_macros = {} 253 self.nskey_uri_global = {} 254 self.ns_local = {} 255 self.wrappers = collections.defaultdict(list) 256 self.cache = {} 257 self.funcs = {} 258 259 def walk(self): 260 out = [] 261 for d, ds, fs in os.walk(self.main): 262 rel = os.path.relpath(d, self.main).replace(os.sep, "/") + "/" 263 ds[:] = [x for x in ds if x not in SKIP_DIRS] 264 if any(p in rel for p in SKIP_PATHS): 265 continue 266 for f in fs: 267 if f.endswith(SRC_EXT): 268 out.append(os.path.join(d, f)) 269 return sorted(out) 270 271 @staticmethod 272 def eligible(path): 273 base = os.path.basename(path) 274 return base.lower() in KEEP_NAMES or not IMPORT_NAME.search(base) 275 276 def uri(self, key, path): 277 key = re.sub(r"_IDX$", "", key) 278 uri = self.ns_local.get(path, {}).get(key) or self.nskey_uri_global.get(key) 279 if uri: 280 return uri 281 if key == "NONE": 282 return "" 283 if key == "XML": 284 return self.toks["XML_N_XML"] 285 return None 286 287 def prepare(self): 288 texts = {} 289 for p in self.walk(): 290 t = read(p) 291 if "XML_NAMESPACE_" not in t: 292 continue 293 texts[p] = t 294 for p, t in texts.items(): 295 if p.endswith((".hxx", ".h")): 296 mc = parse_macros(t) 297 if mc: 298 self.header_macros[os.path.basename(p)] = mc 299 first = {} 300 for p, t in sorted(texts.items(), key=lambda kv: kv[0]): 301 if not self.eligible(p): 302 continue 303 self.files.append(p) 304 m = mask(t) 305 for key, tok in ns_adds(m, t, self.toks): 306 self.ns_local.setdefault(p, {})[key] = tok 307 if p.endswith("xmloff/source/core/xmlexp.cxx"): 308 self.nskey_uri_global[key] = tok 309 else: 310 first.setdefault(key, tok) 311 for key, tok in first.items(): 312 self.nskey_uri_global.setdefault(key, tok) 313 self.texts = texts 314 315 def names(self, arg_masked, arg_clean): 316 """Names in a name argument, and whether anything was left unresolved.""" 317 found, rest = [], arg_masked 318 for m in STR_RE.finditer(arg_clean): 319 if m.group(1): 320 found.append(m.group(1)) 321 for m in TOK_RE.finditer(arg_masked): 322 if m.group(0) in self.toks and not m.group(0).startswith(("XML_NP_", "XML_N_")): 323 found.append(self.toks[m.group(0)]) 324 if not found and re.search(r"\bXML_NP?_[A-Z]", arg_masked): 325 return [], False 326 rest = TOK_RE.sub(" ", STR_RE.sub(" ", arg_clean)) 327 rest = re.sub(r"(?:::)?(?:xmloff::token::|token::|GetXMLToken|RTL_CONSTASCII_USTRINGPARAM|" 328 r"OUString|createFromAscii|rtl::|sal_Char|static_cast<[^>]*>)", " ", rest) 329 rest = re.sub(r"[\s()?:]|\bsal_True\b|\bsal_False\b", "", rest) 330 return found, bool(rest) or not found 331 332 def record(self, path, line, key, names, role, row): 333 uri = self.uri(key, path) 334 if uri is None: 335 self.unres.append((path, line, "unmapped namespace key " + key, row)) 336 return 337 mod = os.path.relpath(path, self.main).split(os.sep)[0] 338 for n in names: 339 e = self.pairs[(uri, n)] 340 e["roles"].add(role) 341 e["mods"].add(mod) 342 e["ex"].append((os.path.relpath(path, self.main), line, role)) 343 344 def scan_file(self, path): 345 text = self.texts[path] 346 spans = directive_spans(text) 347 m = blank(mask(text), spans) 348 clean = blank(text, spans) 349 macros = {} 350 for inc in included(text): 351 macros.update(self.header_macros.get(os.path.basename(inc), {})) 352 macros.update(parse_macros(text)) 353 self.cache[path] = (m, clean, text) 354 line_of = lambda i: text.count("\n", 0, i) + 1 355 snip = lambda a, b: " ".join(clean[a:b].split())[:160] 356 handled = set() 357 opener = self.openers(m) 358 359 funcs = self.funcs[path] = func_defs(m) 360 tokmaps = [(t.end(), match_close(m, t.end() - 1)) 361 for t in re.finditer(r"SvXMLTokenMapEntry\s+\w+\s*\[[^\]]*\]\s*=\s*\{", m)] 362 in_tokmap = lambda i: any(a <= i < b for a, b in tokmaps) 363 364 def wrap(ns_a, ns_b, nm_a, nm_b, role, i): 365 """Register the enclosing function as a wrapper when it forwards its parameters.""" 366 inner = [f for f in funcs if f[2] < i < f[3]] 367 if not inner: 368 return False 369 name, params, b0, b1 = min(inner, key=lambda f: f[3] - f[2]) 370 nm, ns = m[nm_a:nm_b].strip(), m[ns_a:ns_b].strip() 371 if nm not in params or name in EMIT_FUNCS or role == "either": 372 return False 373 if NS_RE.fullmatch(ns): 374 keys, ns_param = [NS_RE.fullmatch(ns).group(1)], None 375 elif ns in params: 376 keys, ns_param = None, ns 377 else: 378 return False 379 module = os.path.relpath(path, self.main).split(os.sep)[0] 380 self.wrappers[(module, name)].append( 381 Wrapper(params.index(nm), len(params), keys, params.index(ns) if ns_param else None, role)) 382 return True 383 384 def pair_from(ns_a, ns_b, nm_a, nm_b, role, ctx_a, ctx_b, i, fixed=None, wrapok=True): 385 if in_tokmap(i): 386 return 387 keys = NS_RE.findall(m[ns_a:ns_b]) 388 names, bad = (fixed, False) if fixed else self.names(m[nm_a:nm_b], clean[nm_a:nm_b]) 389 row = snip(ctx_a, ctx_b) 390 for k in NS_RE.finditer(m[ns_a:ns_b]): 391 handled.add(ns_a + k.start()) 392 if (not keys or bad) and wrapok and not fixed and wrap(ns_a, ns_b, nm_a, nm_b, role, i): 393 return 394 if not keys: 395 self.unres.append((path, line_of(i), "namespace not a literal key", row)) 396 return 397 if bad: 398 self.unres.append((path, line_of(i), "name not a literal token or string", row)) 399 if not names: 400 return 401 if NO_EXPORT.search(clean[ctx_a:ctx_b]): 402 self.skipped.append((path, line_of(i), "not exported (flags)", row)) 403 return 404 for k in keys: 405 self.record(path, line_of(i), k, names, role, row) 406 407 for name, params, b0, b1 in funcs: 408 ns_fn = [f for f in funcs if f[0] == re.sub(r"Name$", "Namespace", name) and name.endswith("AttributeName")] 409 if not ns_fn: 410 continue 411 ns_body = clean[ns_fn[0][2]:ns_fn[0][3]] 412 default = re.findall(r"return\s+XML_NAMESPACE_([A-Z][A-Z0-9_]*)\s*;", ns_body)[-1:] 413 special = {(y if x in ns_fn[0][1] else x): k for x, y, k in re.findall( 414 r"if\s*\(\s*(\w+)\s*==\s*(\w+)\s*\)\s*return\s+XML_NAMESPACE_(\w+)", ns_body)} 415 for cm in re.finditer(r'case\s+(\w+)\s*:\s*return\s+"([^"]+)"', clean[b0:b1]): 416 key = special.get(cm.group(1)) or default and default[0] 417 if key: 418 self.record(path, line_of(b0 + cm.start()), key, [cm.group(2)], "attribute", cm.group(0)) 419 420 for mm in re.finditer(r"\b(%s)\s*\(" % "|".join(EMIT_ATTR), m): 421 o = mm.end() - 1 422 c = match_close(m, o) 423 a = split_args(m, o, c) 424 if len(a) < 2 or (len(a) == 2 and "XML_NAMESPACE_" not in m[a[0][0]:a[0][1]]): 425 continue 426 if DECL_ARG.match(m[a[0][0]:a[0][1]].strip()): 427 continue 428 pair_from(a[0][0], a[0][1], a[1][0], a[1][1], "attribute", mm.start(), c + 1, mm.start()) 429 430 for mm in re.finditer(r"\bSvXMLElementExport\b\s*(?:\w+\s*)?\(", m): 431 o = mm.end() - 1 432 c = match_close(m, o) 433 a = split_args(m, o, c) 434 k = 2 if len(a) >= 6 else 1 435 if len(a) < 3 or len(a) <= k + 1 or DECL_ARG.match(m[a[k][0]:a[k][1]].strip()): 436 continue 437 pair_from(a[k][0], a[k][1], a[k + 1][0], a[k + 1][1], "element", mm.start(), c + 1, mm.start()) 438 439 for mm in re.finditer(r"\bAddAttributeIdLegacy\s*\(", m): 440 o = mm.end() - 1 441 c = match_close(m, o) 442 a = split_args(m, o, c) 443 if len(a) == 2 and not DECL_ARG.match(m[a[0][0]:a[0][1]].strip()): 444 pair_from(a[0][0], a[0][1], 0, 0, "attribute", mm.start(), c + 1, mm.start(), ["id"]) 445 446 for mm in re.finditer(r"\bGetQNameByKey\s*\(", m): 447 o = mm.end() - 1 448 c = match_close(m, o) 449 a = split_args(m, o, c) 450 up = enclosing_call(m, mm.start()) 451 if up and up[0] in EMIT_ATTR and up[1] >= (1 if up[2] == 2 else 2): 452 self.skipped.append((path, line_of(mm.start()), "QName used as an attribute value", snip(mm.start(), c + 1))) 453 for k in NS_RE.finditer(m[a[0][0]:a[0][1]]): 454 handled.add(a[0][0] + k.start()) 455 elif len(a) == 2 and STR_RE.search(clean[a[1][0]:a[1][1]]) and not TOK_RE.search(m[a[1][0]:a[1][1]]): 456 self.skipped.append((path, line_of(mm.start()), "QName with literal local name (a value)", snip(mm.start(), c + 1))) 457 elif len(a) == 2: 458 pair_from(a[0][0], a[0][1], a[1][0], a[1][1], "either", mm.start(), c + 1, mm.start(), wrapok=False) 459 460 for name, mac in macros.items(): 461 for mm in re.finditer(r"\b%s\s*\(" % re.escape(name), m): 462 o = mm.end() - 1 463 c = match_close(m, o) 464 a = split_args(m, o, c) 465 if len(a) <= max(mac.ns_i or 0, mac.tok_i): 466 continue 467 if in_tokmap(mm.start()): 468 continue 469 row = snip(mm.start(), c + 1) 470 ns = mac.ns_fixed or m[a[mac.ns_i][0]:a[mac.ns_i][1]].strip() 471 tk = mac.tok_prefix + clean[a[mac.tok_i][0]:a[mac.tok_i][1]].strip() 472 names, bad = self.names(tk, tk) 473 if not re.fullmatch(r"[A-Z][A-Z0-9_]*", ns) or bad: 474 self.unres.append((path, line_of(mm.start()), "macro argument not a known key/token", row)) 475 continue 476 body = clean[mm.start():c] 477 if NO_EXPORT.search(body): 478 self.skipped.append((path, line_of(mm.start()), "not exported (flags)", row)) 479 continue 480 role = "element" if ELEMENT_ROW.search(body) else "attribute" if PROP_ROW.search(body) else "either" 481 self.record(path, line_of(mm.start()), ns, names, role, row) 482 483 for mm in NS_RE.finditer(m): 484 if mm.start() in handled: 485 continue 486 j = mm.end() 487 while j < len(m) and m[j] in " \t\r\n": 488 j += 1 489 if j >= len(m) or m[j] != ",": 490 continue 491 br = opener.get(mm.start()) 492 if br is None: 493 continue 494 bi, bc = br 495 before = re.search(r"(\w+)\s*(\w*)\s*$", m[max(0, bi - 80):bi]) 496 if m[bi] == "(" and before and (before.group(1) in EMIT_FUNCS or before.group(1) == "SvXMLElementExport" 497 or before.group(2) and before.group(1) == "SvXMLElementExport"): 498 continue 499 a = split_args(m, bi, bc) 500 idx = next(n for n, (s, e) in enumerate(a) if s <= mm.start() < e) 501 if idx + 1 >= len(a) or m[a[idx][0]:a[idx][1]].strip() != mm.group(0): 502 continue 503 row_clean = clean[bi:bc + 1] 504 if m[bi] == "{" and re.search(r"\bXML_TOK_\w+", row_clean): 505 continue 506 if EVENT_ROW.match(row_clean): 507 self.skipped.append((path, line_of(mm.start()), "event-name translation (a value, not a name)", snip(bi, bc + 1))) 508 continue 509 if m[bi] == "{" or PROP_ROW.search(row_clean): 510 role = "element" if ELEMENT_ROW.search(row_clean) else "attribute" 511 if not PROP_ROW.search(row_clean): 512 role = "either" 513 else: 514 role = "either" 515 pair_from(a[idx][0], a[idx][1], a[idx + 1][0], a[idx + 1][1], role, bi, bc + 1, mm.start(), wrapok=False) 516 517 def ns_default(self, module, fname, ns_param_i): 518 """Default namespace key(s) of the ns parameter of fname, from its declarations.""" 519 out = set() 520 for p, (m, clean, text) in self.cache.items(): 521 if os.path.relpath(p, self.main).split(os.sep)[0] != module: 522 continue 523 for mm in re.finditer(r"\b%s\s*\(" % re.escape(fname), m): 524 o = mm.end() - 1 525 a = split_args(m, o, match_close(m, o)) 526 if ns_param_i < len(a): 527 d = re.search(r"=\s*XML_NAMESPACE_([A-Z][A-Z0-9_]*)", m[a[ns_param_i][0]:a[ns_param_i][1]]) 528 if d: 529 out.add(d.group(1)) 530 return out 531 532 def scan_wrapper_calls(self, path, discover): 533 """Resolve calls of wrapper functions; with discover, only register forwarders.""" 534 m, clean, text = self.cache[path] 535 module = os.path.relpath(path, self.main).split(os.sep)[0] 536 line_of = lambda i: text.count("\n", 0, i) + 1 537 found = 0 538 for (mod, fname), ws in list(self.wrappers.items()): 539 if mod != module: 540 continue 541 for mm in re.finditer(r"\b%s\s*\(" % re.escape(fname), m): 542 o = mm.end() - 1 543 c = match_close(m, o) 544 a = split_args(m, o, c) 545 a0 = m[a[0][0]:a[0][1]].strip() 546 if DEF_TAIL.match(m, c + 1) or re.fullmatch(r"[\w:]+(?:\s+[\w:*&]+)+\s*(=.*)?", a0, re.S): 547 continue 548 row = " ".join(clean[mm.start():c + 1].split())[:160] 549 res, fwd = set(), [] 550 for w in ws: 551 if not (w.tok_i < len(a) <= w.nparams): 552 continue 553 ta, tb = a[w.tok_i] 554 names, bad = self.names(m[ta:tb], clean[ta:tb]) 555 if w.keys: 556 keys = tuple(w.keys) 557 elif w.ns_param < len(a): 558 k = NS_RE.fullmatch(m[a[w.ns_param][0]:a[w.ns_param][1]].strip()) 559 keys = (k.group(1),) if k else None 560 else: 561 d = self.ns_default(module, fname, w.ns_param) 562 keys = tuple(d) if len(d) == 1 else None 563 if bad and keys: 564 fwd.append((m[ta:tb].strip(), keys, w.role)) 565 res.add(None if bad or not keys else (tuple(names), keys, w.role)) 566 if discover: 567 inner = [f for f in self.funcs[path] if f[2] < mm.start() < f[3]] 568 if inner and len(fwd) == 1 and len(res) == 1: 569 name, params, b0, b1 = min(inner, key=lambda f: f[3] - f[2]) 570 if fwd[0][0] in params and name not in EMIT_FUNCS: 571 nw = Wrapper(params.index(fwd[0][0]), len(params), list(fwd[0][1]), None, fwd[0][2]) 572 old = self.wrappers[(module, name)] 573 if not any(x.tok_i == nw.tok_i and x.nparams == nw.nparams and x.keys == nw.keys for x in old): 574 old.append(nw) 575 found += 1 576 continue 577 if len(res) != 1 or None in res: 578 if res and not (len(fwd) == 1 and any(f[2] < mm.start() < f[3] and fwd[0][0] in f[1] for f in self.funcs[path])): 579 self.unres.append((path, line_of(mm.start()), "wrapper %s() call not resolvable" % fname, row)) 580 continue 581 names, keys, role = res.pop() 582 for k in keys: 583 self.record(path, line_of(mm.start()), k, list(names), role, row) 584 return found 585 586 @staticmethod 587 def openers(m): 588 """Map every XML_NAMESPACE_ match start to its innermost enclosing bracket.""" 589 starts = {x.start() for x in NS_RE.finditer(m)} 590 out, stack = {}, [] 591 pairs = {} 592 for i, c in enumerate(m): 593 if c in "({": 594 stack.append(i) 595 elif c in ")}": 596 if stack: 597 pairs[stack.pop()] = i 598 elif i in starts: 599 out[i] = stack[-1] if stack else None 600 res = {} 601 for s, o in out.items(): 602 if o is not None and o in pairs: 603 res[s] = (o, pairs[o]) 604 return res 605 606 def write(self, outdir): 607 os.makedirs(outdir, exist_ok=True) 608 rows = [] 609 for (uri, name), e in self.pairs.items(): 610 known = e["roles"] - {"either"} 611 role = known.pop() if len(known) == 1 else "either" 612 ex = sorted(e["ex"], key=lambda x: (x[2] == "either", x[0], x[1]))[0] 613 rows.append((uri, name, role, ",".join(sorted(e["mods"])), "%s:%d" % ex[:2])) 614 rows.sort() 615 with open(os.path.join(outdir, "emit.tsv"), "w") as f: 616 f.write("# ODF export names found statically in %d export-side C++ files under main/.\n" % len(self.files)) 617 f.write("# Rule: .cxx/.hxx/.h under main/ whose name has no 'imp'/'import' (impastp4.cxx kept), outside test/qa/workben/unotest/unoxml/solver," 618 " xmloff/source/transform and filter/source/svg; shared tables (property maps, handlers) included;" 619 " import token-map rows (XML_TOK_) and rows flagged no-export dropped.\n") 620 f.write("# role: element/attribute from the emitting call or property-map flags; either = unknown or seen as both." 621 " Empty ns_uri = XML_NAMESPACE_NONE.\n") 622 f.write("# ns_uri\tlocal\trole\tmodules\texample_file:line\n") 623 for r in rows: 624 f.write("\t".join(r) + "\n") 625 for name, lst in (("emit-unresolved.tsv", self.unres), ("emit-skipped.tsv", self.skipped)): 626 with open(os.path.join(outdir, name), "w") as f: 627 f.write("# file:line\treason\tsnippet\n") 628 for p, ln, why, row in sorted(set(lst)): 629 f.write("%s:%d\t%s\t%s\n" % (os.path.relpath(p, self.main), ln, why, row)) 630 return rows 631 632 633def main(argv): 634 if len(argv) != 3: 635 sys.exit(__doc__) 636 sc = Scanner(argv[1]) 637 sc.prepare() 638 for p in sc.files: 639 sc.scan_file(p) 640 for _ in range(5): 641 if not sum(sc.scan_wrapper_calls(p, True) for p in sc.files): 642 break 643 for p in sc.files: 644 sc.scan_wrapper_calls(p, False) 645 rows = sc.write(argv[2]) 646 roles = collections.Counter(r[2] for r in rows) 647 print("tokens in xmltoken.cxx: %d; files scanned: %d" % (len(sc.toks), len(sc.files))) 648 print("distinct pairs: %d %s" % (len(rows), dict(roles))) 649 print("unresolved sites: %d; skipped rows: %d" % (len(set(sc.unres)), len(set(sc.skipped)))) 650 651 652if __name__ == "__main__": 653 main(sys.argv) 654