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