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