Pegjs: Comportamiento inconsistente del analizador generado

Creado en 8 mar. 2018  ·  15Comentarios  ·  Fuente: pegjs/pegjs

Tipo de problema

  • Informe de error:

Prerrequisitos

  • ¿Puedes reproducir el problema ?: _si_
  • ¿ Buscaste los
  • ¿Revisaste los foros ?: _no_
  • ¿ Realizaste

Descripción

En este momento estoy usando la API de JS para generar el analizador en tiempo de ejecución. Esto funciona bien.

Luego intenté generar el analizador usando la CLI, para evitar generarlo durante el tiempo de ejecución. Sin embargo, cuando lo uso, obtengo errores (~ la mitad de mis pruebas para analizar los errores de lanzamiento de cadenas).

Pasos para reproducir

  1. Mueve la gramática a su propio archivo grammar.pegjs
  2. Genere el analizador usando la CLI
pegjs -o parser.js grammar.pegjs
  1. Elimine peg.generate('...') y reemplácelo con el nuevo analizador
const parser = require('./parser');
parser.parse('...');
  1. Ejecuta las pruebas

Comportamiento esperado:
Esperaría que el analizador generado por la CLI funcione igual que el analizador generado por la API de JS.

Comportamiento real:
Usando la API de JS, cuando paso esta cadena ( 'foo = "bar"' ) al analizador, obtengo el siguiente AST:

{
  kind: 'condition',
  target: 'foo',
  operator: '=',
  value: 'bar',
  valueType: 'string',
  attributeType: undefined
}

Sin embargo, cuando utilizo el analizador "generado" mediante la CLI y paso la misma cadena ( 'foo = "bar"' ) obtengo el siguiente error:

SyntaxError: Expected "(", boolean, date, datetime, number, string, or time but "\"" found.
    at peg$buildStructuredError (/Users/emmenko/xxx/parser.js:446:12)
    at Object.peg$parse [as parse] (/Users/emmenko/xxx/parser.js:2865:11)
    at repl:1:7
    at ContextifyScript.Script.runInThisContext (vm.js:50:33)
    at REPLServer.defaultEval (repl.js:240:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:441:10)
    at emitOne (events.js:121:20)
    at REPLServer.emit (events.js:211:7) 

Software

  • PEG.js: 0.10.0
  • Node.js: 8.9.1
  • NPM o hilo: [email protected]
  • Navegador: Chrome
  • SO: OSX
  • Editor: VSCode
question

Todos 15 comentarios

Bien, lo llenaste correctamente 👍, ahora solo necesitamos la gramática y puedo ayudarte 😄

Aquí tienes:

// GRAMMAR
const parser = peg.generate(`
{
  function getFlattenedValue (value) {
    if (!value) return undefined
    return Array.isArray(value)
      ? value.map(function(v){return v.value})
      : value.value
  }
  function getValueType (value) {
    if (!value) return undefined
    var rawType = value.type
    if (Array.isArray(value))
      rawType = value[0].type
    switch (rawType) {
      case 'string':
      case 'number':
      case 'boolean':
        return rawType
      default:
        return 'string'
    }
  }
  function getAttributeType (target, op, val) {
    if (typeof target === 'string' && target.indexOf('attributes.') === 0) {
      if (!val)
        return undefined
      switch (op) {
        case 'in':
        case 'not in':
          return val[0].type;
        case 'contains':
          return 'set-' + val.type
        default:
          return Array.isArray(val) ? 'set-' + val[0].type : val.type;
      }
    }
  }
  function transformToCondition (target, op, val) {
    return {
      kind: "condition",
      target: target,
      operator: op,
      value: getFlattenedValue(val),
      valueType: getValueType(val),
      attributeType: getAttributeType(target, op, val),
    }
  }

  function createIdentifier (body) {
    return body
      .map(identifiers => identifiers.filter(identifier => (identifier && identifier !== '.'))) // gets raw_identifiers without dots and empty identifiers
      .filter(identifiers => identifiers.length > 0) // filter out empty identifiers arrays
      .map(identifiers => identifiers.join('.'))
      .join('.') // join back to construct the path
  }
}

// ----- DSL Grammar -----
predicate
  = ws exp:expression ws { return exp; }

expression
  = head:term tail:("or" term)*
    {
      if (tail.length === 0) {
        return head;
      }

      return {
        kind: "logical",
        logical: "or",
        conditions: [head].concat(tail.map(function(el){return el[1];})),
      };
    }

term
  = head:factor tail:("and" factor)*
    {
      if (tail.length === 0) {
        return head;
      }

      return {
        kind: "logical",
        logical: "and",
        conditions: [head].concat(tail.map(function(el){return el[1];})),
      };
    }

factor
  = ws negation:"not" ws primary:primary ws
    {
      return {
        kind: "negation",
        condition: primary,
      };
    }
  / ws primary:primary ws { return primary; }

primary
  = basic_comparison
  / list_comparison
  / empty_comparison
  / parens

// ----- Comparators -----
basic_comparison
  = target:val_expression ws op:single_operators ws val:value
    { return transformToCondition(target, op, val); }

list_comparison
  = target:val_expression ws op:list_operators ws val:list_of_values
    { return transformToCondition(target, op, val); }

empty_comparison
  = target:val_expression ws op:empty_operators
    { return transformToCondition(target, op); }

// ----- Operators -----
single_operators
  = "!="
  / "="
  / "<>"
  / ">="
  / ">"
  / "<="
  / "<"
  / "contains"

list_operators
  = "!="
  / "="
  / "<>"
  / "not in"
  / "in"
  / "contains all"
  / "contains any"

empty_operators
  = "is not empty"
  / "is empty"
  / "is not defined"
  / "is defined"

list_of_values
  = ws "(" ws head:value tail:(ws "," ws value)* ws ")" ws
    {
      if (tail.length === 0) {
        return [head];
      }
      return [head].concat(tail.map(function(el){ return el[el.length -1];}));
    }

// ----- Expressions -----
val_expression
  = application_expression
  / constant_expression
  / field_expression

application_expression
  = identifier ws "(" ws function_argument (ws "," ws function_argument)* ws ")"
constant_expression = ws val:value ws { return val; }
field_expression = ws i:identifier ws { return i; }

function_argument
  = expression
  / constant_expression
  / field_expression

value
  = v:boolean { return { type: 'boolean', value: v }; }
  / v:datetime { return { type: 'datetime', value: v }; }
  / v:date { return { type: 'date', value: v }; }
  / v:time { return { type: 'time', value: v }; }
  / v:number { return { type: 'number', value: v }; }
  / v:string { return { type: 'string', value: v }; }

// ----- Common rules -----
parens
  = ws "(" ws ex:expression ws ")" ws { return ex; }

identifier
  = body:((raw_identifier "." escaped_identifier)+ / (raw_identifier "." raw_identifier)+)
    { 
      return createIdentifier(body)
    }
    / i:raw_identifier { return i; }

escaped_identifier
  = "\`" head:raw_identifier tail:("-" raw_identifier)* "\`"
    { return [head].concat(tail.map(function(el){return el.join('');})).join(''); }

raw_identifier = i:[a-zA-Z0-9_]* { return i.join(''); }

ws "whitespace" = [ \\t\\n\\r]*

// ----- Types: booleans -----
boolean "boolean"
  = "false" { return false; }
  / "true" { return true; }

// ----- Types: datetime -----
datetime "datetime"
  =  quotation_mark datetime:datetime_format quotation_mark
    { return datetime.map(function(el){return Array.isArray(el) ? el.join('') : el;}).join(''); }

datetime_format = date_format time_mark time_format zulu_mark
time_mark = "T"
zulu_mark = "Z"

// ----- Types: date -----
date "date"
  =  quotation_mark date:date_format quotation_mark { return date.join("");}

date_format = [0-9][0-9][0-9][0-9] minus [0-9][0-9] minus [0-9][0-9]

// ----- Types: time -----
time "time"
  =  quotation_mark time:time_format quotation_mark { return time.join("");}

time_format = [0-2][0-9] colon [0-5][0-9] colon [0-5][0-9] decimal_point [0-9][0-9][0-9]
colon = ":"

// ----- Types: numbers -----
number "number"
  = minus? int frac? exp? { return parseFloat(text()); }

decimal_point = "."
digit1_9 = [1-9]
e = [eE]
exp = e (minus / plus)? DIGIT+
frac = decimal_point DIGIT+
int = zero / (digit1_9 DIGIT*)
minus = "-"
plus = "+"
zero = "0"

// ----- Types: strings -----
string "string"
  = quotation_mark chars:char* quotation_mark { return chars.join(""); }

char
  = unescaped
  / escape
    sequence:(
        '"'
      / "\\\\"
      / "/"
      / "b" { return "\\b"; }
      / "f" { return "\\f"; }
      / "n" { return "\\n"; }
      / "r" { return "\\r"; }
      / "t" { return "\\t"; }
      / "u" digits:$(HEXDIG HEXDIG HEXDIG HEXDIG)
        { return String.fromCharCode(parseInt(digits, 16)); }
    )
    { return sequence; }

escape = "\\\\"
quotation_mark = '"'
unescaped = [^\\0-\\x1F\\x22\\x5C]
// See RFC 4234, Appendix B (http://tools.ietf.org/html/rfc4234).
DIGIT  = [0-9]
HEXDIG = [0-9a-f]i

Una pequeña adición relacionada con el error. Configuré pegjs través de pegjs-loader . Opera en la API JS bajo el capó llamando parser.generate y también conduce al mismo error.

¡Muchas gracias por el proyecto por cierto!

@emmenko No sé por qué tu gramática funcionaba con la API (seguiré intentando averiguar por qué), pero tu gramática era incorrecta, la regla unescaped debería ser:

unescaped = !'"' [^\\0-\\x1F\\x22\\x5C]

Dime si esto soluciona el problema por tu parte

@tdeekens Si es el mismo error (por ejemplo, Expected ... but "\"" found. ), compruebe si su gramática es correcta o publíquelo aquí

@futagoza @tdeekens y yo estamos en el mismo equipo, así que es el mismo problema 😅

¡Nos mantendremos informados! Gracias por su apoyo hasta ahora 🙏

No sé por qué tu gramática funcionaba con la API.

Para ser honesto, nunca tuvimos ningún problema con eso. ¡Gracias por señalarlo de todos modos!

¿Está funcionando ahora?

Desafortunadamente no ayudó ☹️

Usando su gramática, PEG.js 0.10, Node 8.9.0 y la entrada foo = "bar" , probé esto a través de 3 rutas:

  1. https://pegjs.org/online
  2. API de PEG.js
  3. pegjs CLI

Los 3 mostraron el mismo error: Line 1, column 7: Expected "(", boolean, date, datetime, number, string, or time but "\"" found.

Si cambio su gramática, corrige este error para las 3 rutas:

// orignal
unescaped = [^\\0-\\x1F\\x22\\x5C]

// fixed
unescaped = !'"' [^\\0-\\x1F\\x22\\x5C]

Después de aplicar la regla fija, ¿puede verificar si:

  • recibe el mismo mensaje de error o un error diferente
  • está haciendo algo diferente (o pasos adicionales) de lo que he mencionado
  • ¿Estás usando opciones cuando usas la API de PEG.js?

Además, después de ajustar un poco la entrada, me di cuenta de que su gramática no tiene en cuenta las líneas nuevas como espacios en blanco correctamente, lo más probable es que esto se deba a su regla ws .

EDITAR: Aquí está mi guión de prueba:

/* eslint node/no-unsupported-features: 0 */

"use strict";

const { exec } = require( "child_process" );
const { readFileSync } = require( "fs" );
const { join } = require( "path" );
const { generate } = require( "pegjs" );

function test( parser ) {

    try {

        console.log( parser.parse( `foo = "bar"` ) );

    } catch ( error ) {

        if ( error.name !== "SyntaxError" ) throw error;

        const loc = error.location.start;

        console.log( `Line ${ loc.line }, column ${ loc.column }: ${ error.message }` );

    }

}

const COMMAND = process.argv[ 2 ];
switch ( COMMAND ) {

    case "api":
        test( generate( readFileSync( join( __dirname, "grammar.pegjs" ), "utf8" ) ) );
        break;

    case "cli":
        exec( "node node_modules/pegjs/bin/pegjs -o parser.js grammar.pegjs", error => {

            if ( error ) console.error( error ), process.exit( 1 );

            test( require( "./parser" ) );

        } );
        break;

    default:
        console.error( `Invalid command "${ COMMAND }" passed to test script.` );
        process.exit( 1 );

}

¡Muchas gracias por la respuesta! Lo intentaremos mañana con tu sugerencia y te avisaremos lo antes posible si eso te ayudó. 🙏

Gracias por la respuesta. Primero disculpas por la confusión. Solo quería señalar que el problema también estaba en el cargador de paquetes web. Lo siento que haya causado confusión sobre este tema.

Probamos la mejora. Corrige el analizador en general, pero nos encontramos con un nuevo problema y ahora nos cuesta entender el motivo.

Un ejemplo es de una prueba es (más abajo)

Object {
+   "attributeType": undefined,
    "kind": "condition",
    "operator": "=",
    "target": "foo",
-   "value": "bar",
+   "value": ",b,a,r",
    "valueType": "string",
}

Creemos que el error podría estar de nuestro lado, pero todavía no estamos seguros de dónde. Esto sucede, por ejemplo, con la siguiente entrada

categories.id != ("b33f8e3a-f8d1-476f-a595-2615c4b57556")

que se convierte en

categories.id != (",b,3,3,f,8,e,3,a,-,f,8,d,1,-,4,7,6,f,-,a,5,9,5,-,2,6,1,5,c,4,b,5,7,5,5,6")

cuando se analiza.

Obviamente estaríamos muy agradecidos por una pista, pero también entendemos si puede apoyarnos allí.

Ups, mi error 😨, esto debería arreglar eso

unescaped = !'"' value:[^\\0-\\x1F\\x22\\x5C] { return value; }

Gracias por la respuesta superrápida. Ayuda, pero no cuando se usa la CLI o el cargador de paquetes web que a menudo devuelven el error inicial de SyntaxError: Expected "(", boolean, date, datetime, number, string, or time but "\"" found. . Algo que sucede, por ejemplo, con not(sku = "123") o un ejemplo más complejo lineItemTotal(sku = "SKU1" or list contains all (1,2,3), field.name, "third arg") = "10 EUR" . ¿Podría eso tener algo que ver con la fuga?

Sí, resulta que es por el doble escape. Aquí están las reglas fijas:

ws "whitespace" = [ \t\n\r]*

char
  = unescaped
  / escape
    sequence:(
        '"'
      / "\\"
      / "/"
      / "b" { return "\b"; }
      / "f" { return "\f"; }
      / "n" { return "\n"; }
      / "r" { return "\r"; }
      / "t" { return "\t"; }
      / "u" digits:$(HEXDIG HEXDIG HEXDIG HEXDIG)
        { return String.fromCharCode(parseInt(digits, 16)); }
    )
    { return sequence; }

escape = "\\"

unescaped = !'"' value:[^\0-\x1F\x22\x5C] { return value; }

EDITAR: Parece que es posible que desee trabajar en las reglas que analizan el ejemplo complejo: lineItemTotal(sku = "SKU1" or list contains all (1,2,3), field.name, "third arg") = "10 EUR" , actualmente está saliendo un extraño "kind":"condition" nodo

Muchas gracias por la ayuda y los consejos. Parece solucionar los problemas que teníamos. Analizaremos el aviso con respecto al nodo "condición".

De nada 😄

¿Fue útil esta página
0 / 5 - 0 calificaciones