xref: /trunk/test/odf-validate/formulacheck.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# usage: formulacheck.py <outdir> <formulas.json>
23*0e16a31dSJim Jagielski# ODF 1.2 Part 1 2.2.1 D.3 and 2.2.4 D/E: the style:condition, table:condition, table:expression,
24*0e16a31dSJim Jagielski# table:formula and text:formula values are parsed with the OpenFormula grammar (Part 2 chapter 5)
25*0e16a31dSJim Jagielski# and the condition syntaxes of Part 1 19.468 and 19.596. Syntax only: no semantics, no function arity.
26*0e16a31dSJim Jagielski# Findings are "shall: [rule] ..." or "should: [rule] ...".
27*0e16a31dSJim Jagielskiimport sys, os, re, json, zipfile, collections, unicodedata
28*0e16a31dSJim Jagielskifrom xml.parsers import expat
29*0e16a31dSJim Jagielski
30*0e16a31dSJim JagielskiD = os.path.dirname(os.path.abspath(__file__))
31*0e16a31dSJim JagielskiOF = "urn:oasis:names:tc:opendocument:xmlns:of:1.2"
32*0e16a31dSJim JagielskiNS = "urn:oasis:names:tc:opendocument:xmlns:%s:1.0"
33*0e16a31dSJim JagielskiATTRS = {NS % "style" + " condition": "style:condition", NS % "table" + " condition": "table:condition",
34*0e16a31dSJim Jagielski         NS % "table" + " expression": "table:expression", NS % "table" + " formula": "table:formula",
35*0e16a31dSJim Jagielski         NS % "text" + " formula": "text:formula"}
36*0e16a31dSJim JagielskiSPREADSHEET = NS % "office" + " spreadsheet"
37*0e16a31dSJim JagielskiSTANDARD = {l.strip().upper() for l in open(os.path.join(D, "openformula-functions.txt")) if l.strip() and not l.startswith("#")}
38*0e16a31dSJim Jagielski
39*0e16a31dSJim Jagielski
40*0e16a31dSJim Jagielskiclass Bad(Exception):
41*0e16a31dSJim Jagielski    def __init__(self, pos, msg):
42*0e16a31dSJim Jagielski        Exception.__init__(self, msg)
43*0e16a31dSJim Jagielski        self.pos, self.msg = pos, msg
44*0e16a31dSJim Jagielski
45*0e16a31dSJim Jagielski
46*0e16a31dSJim Jagielskidef is_letter(c):
47*0e16a31dSJim Jagielski    return unicodedata.category(c)[0] == "L" or unicodedata.category(c) == "Nl"
48*0e16a31dSJim Jagielski
49*0e16a31dSJim Jagielski
50*0e16a31dSJim Jagielskidef is_namechar(c):
51*0e16a31dSJim Jagielski    return is_letter(c) or unicodedata.category(c) in ("Nd", "Mn", "Mc", "Me") or c == "_"
52*0e16a31dSJim Jagielski
53*0e16a31dSJim Jagielski
54*0e16a31dSJim Jagielski# Part 2 5.3, 5.4, 5.8, 5.12
55*0e16a31dSJim JagielskiNUMBER = re.compile(r"(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)(?:[eE][-+]?[0-9]+)?")
56*0e16a31dSJim JagielskiSTRING = re.compile(r'"(?:[^"\x00]|"")*"')
57*0e16a31dSJim JagielskiSINGLE = re.compile(r"'(?:[^']|'')+'")
58*0e16a31dSJim JagielskiERROR = re.compile(r"#[A-Z0-9]+(?:[!?]|/(?:[A-Z]|[0-9][!?]))")
59*0e16a31dSJim Jagielski_Q = r"\$?'(?:[^']|'')+'"
60*0e16a31dSJim Jagielski_SN = r"(?:%s|\$?[^\]\. #$']+)" % _Q
61*0e16a31dSJim Jagielski_COL, _ROW = r"\$?[A-Z]+", r"\$?[1-9][0-9]*"
62*0e16a31dSJim Jagielski_CELL = _COL + _ROW
63*0e16a31dSJim Jagielski_SL = r"%s(?:\.(?:%s|%s))*" % (_SN, _CELL, _Q)
64*0e16a31dSJim JagielskiRANGE = re.compile("|".join([
65*0e16a31dSJim Jagielski    r"(?:%s)?\.%s(?::\.%s)?" % (_SL, _CELL, _CELL), r"(?:%s)?\.%s:\.%s" % (_SL, _COL, _COL), r"(?:%s)?\.%s:\.%s" % (_SL, _ROW, _ROW),
66*0e16a31dSJim Jagielski    r"%s\.%s:%s\.%s" % (_SL, _CELL, _SL, _CELL), r"%s\.%s:%s\.%s" % (_SL, _COL, _SL, _COL), r"%s\.%s:%s\.%s" % (_SL, _ROW, _SL, _ROW)]))
67*0e16a31dSJim JagielskiQSHEET = re.compile(_Q)
68*0e16a31dSJim JagielskiCELLREF = re.compile(r"[A-Za-z]+[0-9]+")
69*0e16a31dSJim JagielskiINFIX = ("<>", "<=", ">=", "+", "-", "*", "/", "^", "=", "<", ">", "&", ":", "!", "~")
70*0e16a31dSJim Jagielski# Part 1 19.468, 19.596
71*0e16a31dSJim JagielskiDECIMAL = re.compile(r"[-+]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)")
72*0e16a31dSJim JagielskiRELOP = re.compile(r"<=|>=|!=|<|>|=")
73*0e16a31dSJim JagielskiCNAME = re.compile(r"[a-z][a-z-]*")
74*0e16a31dSJim Jagielski
75*0e16a31dSJim Jagielski
76*0e16a31dSJim Jagielskidef check_iri(iri):
77*0e16a31dSJim Jagielski    """Approximates the RFC 3987 IRI-reference rule: no controls, spaces or the characters excluded from IRIs."""
78*0e16a31dSJim Jagielski    if re.search(r'[\x00-\x20<>"{}|\\^`\x7f]', iri) or re.search(r"%(?![0-9A-Fa-f]{2})", iri):
79*0e16a31dSJim Jagielski        raise ValueError("not an IRI reference")
80*0e16a31dSJim Jagielski
81*0e16a31dSJim Jagielski
82*0e16a31dSJim Jagielskiclass Parser:
83*0e16a31dSJim Jagielski    def __init__(self, text):
84*0e16a31dSJim Jagielski        self.s, self.i, self.funcs, self.ragged = text, 0, [], None
85*0e16a31dSJim Jagielski
86*0e16a31dSJim Jagielski    def fail(self, msg, at=None):
87*0e16a31dSJim Jagielski        raise Bad(self.i if at is None else at, msg)
88*0e16a31dSJim Jagielski
89*0e16a31dSJim Jagielski    def at(self, chars):
90*0e16a31dSJim Jagielski        return self.i < len(self.s) and self.s[self.i] in chars
91*0e16a31dSJim Jagielski
92*0e16a31dSJim Jagielski    def ws(self):
93*0e16a31dSJim Jagielski        while self.at(" \t\n\r"):
94*0e16a31dSJim Jagielski            self.i += 1
95*0e16a31dSJim Jagielski
96*0e16a31dSJim Jagielski    def expect(self, ch):
97*0e16a31dSJim Jagielski        if not self.at(ch):
98*0e16a31dSJim Jagielski            self.fail("%r expected" % ch)
99*0e16a31dSJim Jagielski        self.i += 1
100*0e16a31dSJim Jagielski
101*0e16a31dSJim Jagielski    def end(self):
102*0e16a31dSJim Jagielski        if self.i < len(self.s):
103*0e16a31dSJim Jagielski            self.fail("unexpected %r" % self.s[self.i])
104*0e16a31dSJim Jagielski
105*0e16a31dSJim Jagielski    # 5.2: Formula ::= Intro? Expression
106*0e16a31dSJim Jagielski    def formula(self):
107*0e16a31dSJim Jagielski        if self.at("="):
108*0e16a31dSJim Jagielski            self.i += 1
109*0e16a31dSJim Jagielski            if self.at("="):
110*0e16a31dSJim Jagielski                self.i += 1
111*0e16a31dSJim Jagielski        self.expression()
112*0e16a31dSJim Jagielski        self.end()
113*0e16a31dSJim Jagielski
114*0e16a31dSJim Jagielski    def expression(self):
115*0e16a31dSJim Jagielski        self.ws()
116*0e16a31dSJim Jagielski        self.operand()
117*0e16a31dSJim Jagielski        while True:
118*0e16a31dSJim Jagielski            self.ws()
119*0e16a31dSJim Jagielski            op = next((o for o in INFIX if self.s.startswith(o, self.i)), None)
120*0e16a31dSJim Jagielski            if op is None:
121*0e16a31dSJim Jagielski                return
122*0e16a31dSJim Jagielski            self.i += len(op)
123*0e16a31dSJim Jagielski            self.ws()
124*0e16a31dSJim Jagielski            self.operand()
125*0e16a31dSJim Jagielski
126*0e16a31dSJim Jagielski    def operand(self):
127*0e16a31dSJim Jagielski        while self.at("+-"):
128*0e16a31dSJim Jagielski            self.i += 1
129*0e16a31dSJim Jagielski            self.ws()
130*0e16a31dSJim Jagielski        self.primary()
131*0e16a31dSJim Jagielski        while True:
132*0e16a31dSJim Jagielski            j = self.i
133*0e16a31dSJim Jagielski            self.ws()
134*0e16a31dSJim Jagielski            if self.at("%"):
135*0e16a31dSJim Jagielski                self.i += 1
136*0e16a31dSJim Jagielski            else:
137*0e16a31dSJim Jagielski                self.i = j
138*0e16a31dSJim Jagielski                return
139*0e16a31dSJim Jagielski
140*0e16a31dSJim Jagielski    def primary(self):
141*0e16a31dSJim Jagielski        s, i = self.s, self.i
142*0e16a31dSJim Jagielski        if i >= len(s):
143*0e16a31dSJim Jagielski            self.fail("expression expected")
144*0e16a31dSJim Jagielski        c = s[i]
145*0e16a31dSJim Jagielski        m = NUMBER.match(s, i) if (c in "0123456789.") else None
146*0e16a31dSJim Jagielski        if m:
147*0e16a31dSJim Jagielski            self.i = m.end()
148*0e16a31dSJim Jagielski        elif c == '"':
149*0e16a31dSJim Jagielski            m = STRING.match(s, i)
150*0e16a31dSJim Jagielski            if not m:
151*0e16a31dSJim Jagielski                self.fail("unterminated string")
152*0e16a31dSJim Jagielski            self.i = m.end()
153*0e16a31dSJim Jagielski        elif c == "{":
154*0e16a31dSJim Jagielski            self.array()
155*0e16a31dSJim Jagielski        elif c == "(":
156*0e16a31dSJim Jagielski            self.i += 1
157*0e16a31dSJim Jagielski            self.expression()
158*0e16a31dSJim Jagielski            self.expect(")")
159*0e16a31dSJim Jagielski        elif c == "[":
160*0e16a31dSJim Jagielski            self.reference()
161*0e16a31dSJim Jagielski        elif c == "#":
162*0e16a31dSJim Jagielski            m = ERROR.match(s, i)
163*0e16a31dSJim Jagielski            if not m:
164*0e16a31dSJim Jagielski                self.fail("malformed error constant")
165*0e16a31dSJim Jagielski            self.i = m.end()
166*0e16a31dSJim Jagielski        elif c in "'$":
167*0e16a31dSJim Jagielski            self.quoted_or_named()
168*0e16a31dSJim Jagielski        elif is_letter(c):
169*0e16a31dSJim Jagielski            self.name()
170*0e16a31dSJim Jagielski        else:
171*0e16a31dSJim Jagielski            self.fail("unexpected %r" % c)
172*0e16a31dSJim Jagielski
173*0e16a31dSJim Jagielski    def array(self):
174*0e16a31dSJim Jagielski        self.i += 1
175*0e16a31dSJim Jagielski        widths = []
176*0e16a31dSJim Jagielski        while True:
177*0e16a31dSJim Jagielski            n = 1
178*0e16a31dSJim Jagielski            self.expression()
179*0e16a31dSJim Jagielski            while self.at(";"):
180*0e16a31dSJim Jagielski                self.i += 1
181*0e16a31dSJim Jagielski                self.expression()
182*0e16a31dSJim Jagielski                n += 1
183*0e16a31dSJim Jagielski            widths.append(n)
184*0e16a31dSJim Jagielski            if self.at("|"):
185*0e16a31dSJim Jagielski                self.i += 1
186*0e16a31dSJim Jagielski                continue
187*0e16a31dSJim Jagielski            break
188*0e16a31dSJim Jagielski        self.expect("}")
189*0e16a31dSJim Jagielski        if len(set(widths)) > 1:
190*0e16a31dSJim Jagielski            self.ragged = widths
191*0e16a31dSJim Jagielski
192*0e16a31dSJim Jagielski    def name(self):
193*0e16a31dSJim Jagielski        s, j = self.s, self.i
194*0e16a31dSJim Jagielski        k = j
195*0e16a31dSJim Jagielski        while k < len(s) and (is_namechar(s[k]) or s[k] == "."):
196*0e16a31dSJim Jagielski            k += 1
197*0e16a31dSJim Jagielski        if k < len(s) and s[k] == "(":
198*0e16a31dSJim Jagielski            self.funcs.append(s[j:k])
199*0e16a31dSJim Jagielski            self.i = k + 1
200*0e16a31dSJim Jagielski            self.params()
201*0e16a31dSJim Jagielski            return
202*0e16a31dSJim Jagielski        ident = s[j:k].split(".", 1)[0]
203*0e16a31dSJim Jagielski        self.i = j + len(ident)
204*0e16a31dSJim Jagielski        if self.i == k and re.match(r"[ \t\n\r]+\(", s[k:]):
205*0e16a31dSJim Jagielski            self.fail("whitespace between function name and '('")
206*0e16a31dSJim Jagielski        self.check_identifier(ident)
207*0e16a31dSJim Jagielski
208*0e16a31dSJim Jagielski    def check_identifier(self, ident):
209*0e16a31dSJim Jagielski        if CELLREF.fullmatch(ident):
210*0e16a31dSJim Jagielski            self.fail("%r looks like a cell reference; references are written in square brackets" % ident, self.i - len(ident))
211*0e16a31dSJim Jagielski        if ident.lower() in ("true", "false"):
212*0e16a31dSJim Jagielski            self.fail("%r is not an identifier; use %s()" % (ident, ident.upper()), self.i - len(ident))
213*0e16a31dSJim Jagielski
214*0e16a31dSJim Jagielski    def params(self):
215*0e16a31dSJim Jagielski        while True:
216*0e16a31dSJim Jagielski            self.ws()
217*0e16a31dSJim Jagielski            if self.at(");"):
218*0e16a31dSJim Jagielski                pass
219*0e16a31dSJim Jagielski            else:
220*0e16a31dSJim Jagielski                self.expression()
221*0e16a31dSJim Jagielski                self.ws()
222*0e16a31dSJim Jagielski            if self.at(";"):
223*0e16a31dSJim Jagielski                self.i += 1
224*0e16a31dSJim Jagielski                continue
225*0e16a31dSJim Jagielski            self.expect(")")
226*0e16a31dSJim Jagielski            return
227*0e16a31dSJim Jagielski
228*0e16a31dSJim Jagielski    # 5.8, 5.10, 5.11
229*0e16a31dSJim Jagielski    def source(self):
230*0e16a31dSJim Jagielski        """Source ::= "'" IRI "'" "#" at the current position, if there is one."""
231*0e16a31dSJim Jagielski        m = SINGLE.match(self.s, self.i)
232*0e16a31dSJim Jagielski        if m and self.s.startswith("#", m.end()):
233*0e16a31dSJim Jagielski            try:
234*0e16a31dSJim Jagielski                check_iri(m.group(0)[1:-1].replace("''", "'"))
235*0e16a31dSJim Jagielski            except ValueError as e:
236*0e16a31dSJim Jagielski                self.fail("source: %s" % e)
237*0e16a31dSJim Jagielski            self.i = m.end() + 1
238*0e16a31dSJim Jagielski            return True
239*0e16a31dSJim Jagielski        return False
240*0e16a31dSJim Jagielski
241*0e16a31dSJim Jagielski    def simple_named(self):
242*0e16a31dSJim Jagielski        """SimpleNamedExpression ::= Identifier | '$$' (Identifier | SingleQuoted)"""
243*0e16a31dSJim Jagielski        if self.s.startswith("$$", self.i):
244*0e16a31dSJim Jagielski            self.i += 2
245*0e16a31dSJim Jagielski            m = SINGLE.match(self.s, self.i)
246*0e16a31dSJim Jagielski            if m:
247*0e16a31dSJim Jagielski                self.i = m.end()
248*0e16a31dSJim Jagielski                return
249*0e16a31dSJim Jagielski        self.identifier()
250*0e16a31dSJim Jagielski
251*0e16a31dSJim Jagielski    def quoted_or_named(self):
252*0e16a31dSJim Jagielski        s = self.s
253*0e16a31dSJim Jagielski        if self.source():
254*0e16a31dSJim Jagielski            m = QSHEET.match(s, self.i)
255*0e16a31dSJim Jagielski            if m and s.startswith(".", m.end()):
256*0e16a31dSJim Jagielski                self.i = m.end() + 1
257*0e16a31dSJim Jagielski            self.simple_named()
258*0e16a31dSJim Jagielski            return
259*0e16a31dSJim Jagielski        if s.startswith("$$", self.i):
260*0e16a31dSJim Jagielski            self.simple_named()
261*0e16a31dSJim Jagielski            return
262*0e16a31dSJim Jagielski        m = QSHEET.match(s, self.i)
263*0e16a31dSJim Jagielski        if not m:
264*0e16a31dSJim Jagielski            self.fail("'$' must be '$$' or precede a quoted sheet name")
265*0e16a31dSJim Jagielski        j = m.end()
266*0e16a31dSJim Jagielski        k = j
267*0e16a31dSJim Jagielski        while k < len(s) and s[k] in " \t\n\r":
268*0e16a31dSJim Jagielski            k += 1
269*0e16a31dSJim Jagielski        if s.startswith(".", j):
270*0e16a31dSJim Jagielski            self.i = j + 1
271*0e16a31dSJim Jagielski            self.simple_named()
272*0e16a31dSJim Jagielski        elif s[self.i] != "$" and s.startswith("!!", k):
273*0e16a31dSJim Jagielski            self.i = k + 2
274*0e16a31dSJim Jagielski            self.ws()
275*0e16a31dSJim Jagielski            m = SINGLE.match(s, self.i)
276*0e16a31dSJim Jagielski            if not m:
277*0e16a31dSJim Jagielski                self.fail("quoted label expected after '!!'")
278*0e16a31dSJim Jagielski            self.i = m.end()
279*0e16a31dSJim Jagielski        elif s[self.i] == "$":
280*0e16a31dSJim Jagielski            self.fail("a quoted sheet name must be followed by '.'")
281*0e16a31dSJim Jagielski        else:
282*0e16a31dSJim Jagielski            self.i = j
283*0e16a31dSJim Jagielski
284*0e16a31dSJim Jagielski    def identifier(self):
285*0e16a31dSJim Jagielski        s, j = self.s, self.i
286*0e16a31dSJim Jagielski        if j >= len(s) or not is_letter(s[j]):
287*0e16a31dSJim Jagielski            self.fail("identifier expected")
288*0e16a31dSJim Jagielski        k = j
289*0e16a31dSJim Jagielski        while k < len(s) and is_namechar(s[k]):
290*0e16a31dSJim Jagielski            k += 1
291*0e16a31dSJim Jagielski        self.i = k
292*0e16a31dSJim Jagielski        self.check_identifier(s[j:k])
293*0e16a31dSJim Jagielski
294*0e16a31dSJim Jagielski    def reference(self):
295*0e16a31dSJim Jagielski        s = self.s
296*0e16a31dSJim Jagielski        j = self.i + 1
297*0e16a31dSJim Jagielski        k = j
298*0e16a31dSJim Jagielski        while k < len(s) and s[k] != "]":
299*0e16a31dSJim Jagielski            if s[k] == "'":
300*0e16a31dSJim Jagielski                m = SINGLE.match(s, k)
301*0e16a31dSJim Jagielski                if not m:
302*0e16a31dSJim Jagielski                    self.fail("unterminated quote in reference")
303*0e16a31dSJim Jagielski                k = m.end()
304*0e16a31dSJim Jagielski            else:
305*0e16a31dSJim Jagielski                k += 1
306*0e16a31dSJim Jagielski        if k >= len(s):
307*0e16a31dSJim Jagielski            self.fail("reference without ']'", self.i)
308*0e16a31dSJim Jagielski        inner = s[j:k]
309*0e16a31dSJim Jagielski        sub = Parser(inner)
310*0e16a31dSJim Jagielski        sub.i = 0
311*0e16a31dSJim Jagielski        try:
312*0e16a31dSJim Jagielski            if inner == "#REF!":
313*0e16a31dSJim Jagielski                pass
314*0e16a31dSJim Jagielski            else:
315*0e16a31dSJim Jagielski                sub.source()
316*0e16a31dSJim Jagielski                if not RANGE.fullmatch(inner, sub.i):
317*0e16a31dSJim Jagielski                    raise Bad(j + sub.i, "malformed reference [%s]" % inner)
318*0e16a31dSJim Jagielski        except Bad as e:
319*0e16a31dSJim Jagielski            raise Bad(j + e.pos if e.pos < len(inner) else j, e.msg)
320*0e16a31dSJim Jagielski        self.i = k + 1
321*0e16a31dSJim Jagielski
322*0e16a31dSJim Jagielski    # Part 1 19.468 / 19.596 value, condition
323*0e16a31dSJim Jagielski    def value(self):
324*0e16a31dSJim Jagielski        s, save = self.s, self.i
325*0e16a31dSJim Jagielski        for rx in (DECIMAL, STRING):
326*0e16a31dSJim Jagielski            m = rx.match(s, save)
327*0e16a31dSJim Jagielski            if m:
328*0e16a31dSJim Jagielski                j = m.end()
329*0e16a31dSJim Jagielski                while j < len(s) and s[j] in " \t\n\r":
330*0e16a31dSJim Jagielski                    j += 1
331*0e16a31dSJim Jagielski                if j >= len(s) or s[j] in ",)":
332*0e16a31dSJim Jagielski                    self.i = j
333*0e16a31dSJim Jagielski                    return
334*0e16a31dSJim Jagielski        self.i = save
335*0e16a31dSJim Jagielski        self.expression()
336*0e16a31dSJim Jagielski
337*0e16a31dSJim Jagielski    def cname(self):
338*0e16a31dSJim Jagielski        self.ws()
339*0e16a31dSJim Jagielski        m = CNAME.match(self.s, self.i)
340*0e16a31dSJim Jagielski        if not m:
341*0e16a31dSJim Jagielski            self.fail("condition expected")
342*0e16a31dSJim Jagielski        self.i = m.end()
343*0e16a31dSJim Jagielski        return m.group(0)
344*0e16a31dSJim Jagielski
345*0e16a31dSJim Jagielski    def call(self, nargs):
346*0e16a31dSJim Jagielski        """'(' value {',' value} ')' or '()'"""
347*0e16a31dSJim Jagielski        self.expect("(")
348*0e16a31dSJim Jagielski        self.ws()
349*0e16a31dSJim Jagielski        if nargs == 0:
350*0e16a31dSJim Jagielski            self.expect(")")
351*0e16a31dSJim Jagielski            return
352*0e16a31dSJim Jagielski        for n in range(nargs):
353*0e16a31dSJim Jagielski            if n:
354*0e16a31dSJim Jagielski                self.ws()
355*0e16a31dSJim Jagielski                self.expect(",")
356*0e16a31dSJim Jagielski            self.ws()
357*0e16a31dSJim Jagielski            self.value()
358*0e16a31dSJim Jagielski        self.ws()
359*0e16a31dSJim Jagielski        self.expect(")")
360*0e16a31dSJim Jagielski
361*0e16a31dSJim Jagielski    def op_value(self):
362*0e16a31dSJim Jagielski        self.ws()
363*0e16a31dSJim Jagielski        if not RELOP.match(self.s, self.i):
364*0e16a31dSJim Jagielski            self.fail("relational operator expected")
365*0e16a31dSJim Jagielski        self.i = RELOP.match(self.s, self.i).end()
366*0e16a31dSJim Jagielski        self.ws()
367*0e16a31dSJim Jagielski        self.value()
368*0e16a31dSJim Jagielski
369*0e16a31dSJim Jagielski    def style_condition(self):
370*0e16a31dSJim Jagielski        name = self.cname()
371*0e16a31dSJim Jagielski        if name in ("footnote", "endnote", "header", "footer", "section", "table", "table-header"):
372*0e16a31dSJim Jagielski            self.call(0)
373*0e16a31dSJim Jagielski        elif name in ("list-level", "outline-level"):
374*0e16a31dSJim Jagielski            self.call(0)
375*0e16a31dSJim Jagielski            self.ws()
376*0e16a31dSJim Jagielski            self.expect("=")
377*0e16a31dSJim Jagielski            self.ws()
378*0e16a31dSJim Jagielski            m = re.compile(r"\+?0*[1-9][0-9]*").match(self.s, self.i)
379*0e16a31dSJim Jagielski            if not m:
380*0e16a31dSJim Jagielski                self.fail("positive integer expected")
381*0e16a31dSJim Jagielski            self.i = m.end()
382*0e16a31dSJim Jagielski        elif name == "value":
383*0e16a31dSJim Jagielski            self.call(0)
384*0e16a31dSJim Jagielski            self.ws()
385*0e16a31dSJim Jagielski            m = RELOP.match(self.s, self.i)
386*0e16a31dSJim Jagielski            if not m:
387*0e16a31dSJim Jagielski                self.fail("relational operator expected")
388*0e16a31dSJim Jagielski            self.i = m.end()
389*0e16a31dSJim Jagielski            self.ws()
390*0e16a31dSJim Jagielski            m = re.compile(r"true|false").match(self.s, self.i) or DECIMAL.match(self.s, self.i)
391*0e16a31dSJim Jagielski            if not m:
392*0e16a31dSJim Jagielski                self.fail("number, true or false expected")
393*0e16a31dSJim Jagielski            self.i = m.end()
394*0e16a31dSJim Jagielski        elif name == "cell-content":
395*0e16a31dSJim Jagielski            self.call(0)
396*0e16a31dSJim Jagielski            self.op_value()
397*0e16a31dSJim Jagielski        elif name in ("cell-content-is-between", "cell-content-is-not-between"):
398*0e16a31dSJim Jagielski            self.call(2)
399*0e16a31dSJim Jagielski        elif name == "is-true-formula":
400*0e16a31dSJim Jagielski            self.call_expression()
401*0e16a31dSJim Jagielski        else:
402*0e16a31dSJim Jagielski            self.fail("unknown condition %r" % name, self.i - len(name))
403*0e16a31dSJim Jagielski        self.ws()
404*0e16a31dSJim Jagielski        self.end()
405*0e16a31dSJim Jagielski
406*0e16a31dSJim Jagielski    def call_expression(self):
407*0e16a31dSJim Jagielski        self.expect("(")
408*0e16a31dSJim Jagielski        self.expression()
409*0e16a31dSJim Jagielski        self.expect(")")
410*0e16a31dSJim Jagielski
411*0e16a31dSJim Jagielski    def value_condition(self):
412*0e16a31dSJim Jagielski        name = self.cname()
413*0e16a31dSJim Jagielski        if name == "cell-content":
414*0e16a31dSJim Jagielski            self.call(0)
415*0e16a31dSJim Jagielski            self.op_value()
416*0e16a31dSJim Jagielski        elif name in ("cell-content-is-between", "cell-content-is-not-between"):
417*0e16a31dSJim Jagielski            self.call(2)
418*0e16a31dSJim Jagielski        elif name == "is-true-formula":
419*0e16a31dSJim Jagielski            self.call_expression()
420*0e16a31dSJim Jagielski        else:
421*0e16a31dSJim Jagielski            self.fail("value condition expected, found %r" % name, self.i - len(name))
422*0e16a31dSJim Jagielski
423*0e16a31dSJim Jagielski    def string_list(self):
424*0e16a31dSJim Jagielski        """list of string entries separated by ';'; leaves the position before ')'"""
425*0e16a31dSJim Jagielski        s, j = self.s, self.i
426*0e16a31dSJim Jagielski        while True:
427*0e16a31dSJim Jagielski            m = STRING.match(s, j)
428*0e16a31dSJim Jagielski            if not m:
429*0e16a31dSJim Jagielski                return False
430*0e16a31dSJim Jagielski            k = m.end()
431*0e16a31dSJim Jagielski            while k < len(s) and s[k] in " \t\n\r":
432*0e16a31dSJim Jagielski                k += 1
433*0e16a31dSJim Jagielski            if s.startswith(";", k):
434*0e16a31dSJim Jagielski                j = k + 1
435*0e16a31dSJim Jagielski                while j < len(s) and s[j] in " \t\n\r":
436*0e16a31dSJim Jagielski                    j += 1
437*0e16a31dSJim Jagielski            elif s.startswith(")", k):
438*0e16a31dSJim Jagielski                self.i = k
439*0e16a31dSJim Jagielski                return True
440*0e16a31dSJim Jagielski            else:
441*0e16a31dSJim Jagielski                return False
442*0e16a31dSJim Jagielski
443*0e16a31dSJim Jagielski    def validation_condition(self):
444*0e16a31dSJim Jagielski        name = self.cname()
445*0e16a31dSJim Jagielski        if name == "cell-content-text-length":
446*0e16a31dSJim Jagielski            self.call(0)
447*0e16a31dSJim Jagielski            self.op_value()
448*0e16a31dSJim Jagielski        elif name in ("cell-content-text-length-is-between", "cell-content-text-length-is-not-between"):
449*0e16a31dSJim Jagielski            self.call(2)
450*0e16a31dSJim Jagielski        elif name == "cell-content-is-in-list":
451*0e16a31dSJim Jagielski            self.expect("(")
452*0e16a31dSJim Jagielski            self.ws()
453*0e16a31dSJim Jagielski            if not self.string_list():
454*0e16a31dSJim Jagielski                self.expression()
455*0e16a31dSJim Jagielski            self.expect(")")
456*0e16a31dSJim Jagielski        elif name in ("cell-content-is-decimal-number", "cell-content-is-whole-number", "cell-content-is-date", "cell-content-is-time"):
457*0e16a31dSJim Jagielski            self.call(0)
458*0e16a31dSJim Jagielski            m = re.compile(r"\s+and\s+").match(self.s, self.i)
459*0e16a31dSJim Jagielski            if not m:
460*0e16a31dSJim Jagielski                self.fail("'and' expected")
461*0e16a31dSJim Jagielski            self.i = m.end()
462*0e16a31dSJim Jagielski            self.value_condition()
463*0e16a31dSJim Jagielski        elif name == "is-true-formula":
464*0e16a31dSJim Jagielski            self.call_expression()
465*0e16a31dSJim Jagielski        else:
466*0e16a31dSJim Jagielski            self.fail("unknown condition %r" % name, self.i - len(name))
467*0e16a31dSJim Jagielski        self.ws()
468*0e16a31dSJim Jagielski        self.end()
469*0e16a31dSJim Jagielski
470*0e16a31dSJim Jagielski
471*0e16a31dSJim Jagielskidef check_value(attr, body, has_prefix):
472*0e16a31dSJim Jagielski    """Returns (None | (pos, message), [function names], ragged column counts | None)."""
473*0e16a31dSJim Jagielski    p = Parser(body)
474*0e16a31dSJim Jagielski    try:
475*0e16a31dSJim Jagielski        if attr in ("table:formula", "text:formula"):
476*0e16a31dSJim Jagielski            p.formula()
477*0e16a31dSJim Jagielski        elif attr == "table:expression":
478*0e16a31dSJim Jagielski            if has_prefix and not body.startswith("="):
479*0e16a31dSJim Jagielski                raise Bad(0, "an expression with a namespace prefix must start with '=' (19.635)")
480*0e16a31dSJim Jagielski            p.formula()
481*0e16a31dSJim Jagielski        elif attr == "style:condition":
482*0e16a31dSJim Jagielski            p.style_condition()
483*0e16a31dSJim Jagielski        else:
484*0e16a31dSJim Jagielski            p.validation_condition()
485*0e16a31dSJim Jagielski    except Bad as e:
486*0e16a31dSJim Jagielski        return (e.pos, e.msg), p.funcs, p.ragged
487*0e16a31dSJim Jagielski    return None, p.funcs, p.ragged
488*0e16a31dSJim Jagielski
489*0e16a31dSJim Jagielski
490*0e16a31dSJim JagielskiNCPREFIX = re.compile(r"([^\W\d][\w.\-]*):")
491*0e16a31dSJim Jagielski
492*0e16a31dSJim Jagielski
493*0e16a31dSJim Jagielskidef check_stream(data, stats):
494*0e16a31dSJim Jagielski    """Returns [(line, col, level, rule, message)]."""
495*0e16a31dSJim Jagielski    out = []
496*0e16a31dSJim Jagielski    scope = collections.defaultdict(list)
497*0e16a31dSJim Jagielski    state = {"depth": 0, "ss": None}
498*0e16a31dSJim Jagielski    p = expat.ParserCreate(namespace_separator=" ")
499*0e16a31dSJim Jagielski    p.StartNamespaceDeclHandler = lambda pre, uri: scope[pre].append(uri)
500*0e16a31dSJim Jagielski    p.EndNamespaceDeclHandler = lambda pre: scope[pre].pop()
501*0e16a31dSJim Jagielski
502*0e16a31dSJim Jagielski    def start(name, attrs):
503*0e16a31dSJim Jagielski        state["depth"] += 1
504*0e16a31dSJim Jagielski        if name == SPREADSHEET:
505*0e16a31dSJim Jagielski            state["ss"] = state["depth"]
506*0e16a31dSJim Jagielski        for key, value in attrs.items():
507*0e16a31dSJim Jagielski            attr = ATTRS.get(key)
508*0e16a31dSJim Jagielski            if attr is None:
509*0e16a31dSJim Jagielski                continue
510*0e16a31dSJim Jagielski            line, col = p.CurrentLineNumber, p.CurrentColumnNumber
511*0e16a31dSJim Jagielski
512*0e16a31dSJim Jagielski            def add(level, rule, msg):
513*0e16a31dSJim Jagielski                out.append((line, col, level, rule, '%s="%s": %s' % (attr, value if len(value) < 80 else value[:77] + "...", msg)))
514*0e16a31dSJim Jagielski            m = NCPREFIX.match(value)
515*0e16a31dSJim Jagielski            body, has_prefix = value, False
516*0e16a31dSJim Jagielski            if m:
517*0e16a31dSJim Jagielski                pre = m.group(1)
518*0e16a31dSJim Jagielski                uri = scope[pre][-1] if scope.get(pre) else None
519*0e16a31dSJim Jagielski                stats[(attr, pre if uri is None else uri)] += 1
520*0e16a31dSJim Jagielski                if uri is None:
521*0e16a31dSJim Jagielski                    add("shall", "formula-prefix-unbound", "namespace prefix \"%s\" is not bound" % pre)
522*0e16a31dSJim Jagielski                    continue
523*0e16a31dSJim Jagielski                if uri != OF:
524*0e16a31dSJim Jagielski                    if attr == "table:formula" and state["ss"] is not None:
525*0e16a31dSJim Jagielski                        add("shall", "formula-namespace", "bound to <%s>, not to the of:1.2 namespace (2.2.4 D)" % uri)
526*0e16a31dSJim Jagielski                    else:
527*0e16a31dSJim Jagielski                        add("should", "formula-foreign-namespace", "bound to <%s>; the syntax is not defined by Part 2" % uri)
528*0e16a31dSJim Jagielski                    continue
529*0e16a31dSJim Jagielski                body, has_prefix = value[m.end():], True
530*0e16a31dSJim Jagielski            else:
531*0e16a31dSJim Jagielski                stats[(attr, "(none)")] += 1
532*0e16a31dSJim Jagielski            err, funcs, ragged = check_value(attr, body, has_prefix)
533*0e16a31dSJim Jagielski            if err:
534*0e16a31dSJim Jagielski                pos, msg = err
535*0e16a31dSJim Jagielski                add("shall", "formula-syntax", "%s at character %d" % (msg, pos + (len(value) - len(body)) + 1))
536*0e16a31dSJim Jagielski            if ragged:
537*0e16a31dSJim Jagielski                add("should", "formula-array-ragged", "inline array rows have %s columns (5.13)" % "/".join(map(str, ragged)))
538*0e16a31dSJim Jagielski            for f in funcs:
539*0e16a31dSJim Jagielski                if f.upper() not in STANDARD and "." not in f:
540*0e16a31dSJim Jagielski                    add("should", "formula-function-unprefixed", "%s is not a function of Part 2 and has no origin prefix (5.7)" % f)
541*0e16a31dSJim Jagielski                if f != f.upper():
542*0e16a31dSJim Jagielski                    add("should", "formula-function-case", "%s: function names should be written in upper case (6.1)" % f)
543*0e16a31dSJim Jagielski    p.StartElementHandler = start
544*0e16a31dSJim Jagielski
545*0e16a31dSJim Jagielski    def end(name):
546*0e16a31dSJim Jagielski        if state["ss"] == state["depth"]:
547*0e16a31dSJim Jagielski            state["ss"] = None
548*0e16a31dSJim Jagielski        state["depth"] -= 1
549*0e16a31dSJim Jagielski    p.EndElementHandler = end
550*0e16a31dSJim Jagielski    try:
551*0e16a31dSJim Jagielski        p.Parse(data, True)
552*0e16a31dSJim Jagielski    except expat.ExpatError as e:
553*0e16a31dSJim Jagielski        out.append((e.lineno, e.offset, "shall", "formula-stream", "not namespace-well-formed: " + expat.ErrorString(e.code)))
554*0e16a31dSJim Jagielski    return out
555*0e16a31dSJim Jagielski
556*0e16a31dSJim Jagielski
557*0e16a31dSJim Jagielskidef main():
558*0e16a31dSJim Jagielski    outdir, fjson = sys.argv[1], sys.argv[2]
559*0e16a31dSJim Jagielski    if not os.path.abspath(fjson).startswith(os.path.join(D, "runs") + os.sep):
560*0e16a31dSJim Jagielski        sys.exit("formulacheck.py: <formulas.json> must live under " + os.path.join(D, "runs"))
561*0e16a31dSJim Jagielski    errors, bad, stats = [], [], collections.Counter()
562*0e16a31dSJim Jagielski    pkgs = sorted(p for p in os.listdir(outdir) if not p.startswith("."))
563*0e16a31dSJim Jagielski    for pkg in pkgs:
564*0e16a31dSJim Jagielski        try:
565*0e16a31dSJim Jagielski            z = zipfile.ZipFile(os.path.join(outdir, pkg))
566*0e16a31dSJim Jagielski        except Exception:
567*0e16a31dSJim Jagielski            bad.append(pkg); continue
568*0e16a31dSJim Jagielski        for name in z.namelist():
569*0e16a31dSJim Jagielski            if name.rsplit("/", 1)[-1] not in ("content.xml", "styles.xml", "settings.xml"):
570*0e16a31dSJim Jagielski                continue
571*0e16a31dSJim Jagielski            for line, col, level, rule, msg in check_stream(z.read(name), stats):
572*0e16a31dSJim Jagielski                errors.append([pkg + "/" + name, line, col, "%s: [%s] %s" % (level, rule, msg)])
573*0e16a31dSJim Jagielski    json.dump({"packages": pkgs, "bad": bad, "checked": {"%s %s" % k: v for k, v in sorted(stats.items())}, "errors": errors}, open(fjson, "w"))
574*0e16a31dSJim Jagielski    shall = [e for e in errors if e[3].startswith("shall:")]
575*0e16a31dSJim Jagielski    print(f"formula check: {sum(stats.values())} values checked; {len(shall)} shall and {len(errors) - len(shall)} should findings in "
576*0e16a31dSJim Jagielski          f"{len({e[0].split('/')[0] for e in errors})} of {len(pkgs)} packages, unreadable: {bad}")
577*0e16a31dSJim Jagielski    for (attr, ns), v in sorted(stats.items()):
578*0e16a31dSJim Jagielski        print(f"  {v:6d} {attr} {ns}")
579*0e16a31dSJim Jagielski    rules = collections.Counter(e[3].split("]")[0] + "]" for e in errors)
580*0e16a31dSJim Jagielski    for k, v in sorted(rules.items()):
581*0e16a31dSJim Jagielski        print(f"  {v:6d} {k}")
582*0e16a31dSJim Jagielski
583*0e16a31dSJim Jagielski
584*0e16a31dSJim Jagielskiif __name__ == "__main__":
585*0e16a31dSJim Jagielski    main()
586