Grupos no participantes:un desastre entre navegadores

Los problemas entre navegadores relacionados con el manejo de grupos de captura no participantes de expresiones regulares (que llamaré NPCG) presentan varios desafíos. Para empezar, el estándar apesta, y los tres navegadores más importantes (IE, Firefox, Safari) no respetan las reglas a su manera.

Primero, debo explicar qué son los NPCG, ya que parece que incluso algunos usuarios experimentados de expresiones regulares no son completamente conscientes o no entienden el concepto. Suponiendo que ya está familiarizado con la idea de los paréntesis de captura y de no captura (consulte esta página si necesita un repaso), tenga en cuenta que los NPCG son diferentes de los grupos que capturan un valor de longitud cero (es decir, una cadena vacía). Esto es probablemente más fácil de explicar mostrando algunos ejemplos...

Las siguientes expresiones regulares contienen potencialmente NPCG (dependiendo de los datos que se ejecutan), porque los grupos de captura no están obligados a participar:

  • /(x)?/
  • /(x)*/
  • /(x){0,2}/
  • /(x)|(y)/ — Si coincide, se garantiza que contiene exactamente un NPCG.
  • /(?!(x))/ — Si esto coincide (que, por sí solo, lo hará al menos al final de la cadena), se garantiza que contendrá un NPCG, porque el patrón solo tiene éxito si la coincidencia de "x" falla.
  • /()??/ — Esto está garantizado para coincidir dentro de cualquier cadena y contener un NPCG, debido al uso de un ?? perezoso cuantificador en un grupo de captura para un valor de longitud cero.

Por otro lado, estos nunca contendrán un NPCG, porque aunque se les permite coincidir con un valor de longitud cero, los grupos de captura son requeridos para participar:

  • /(x?)/
  • /(x*)/
  • /(x{0,2})/
  • /((?:xx)?)/ –o– /(xx|)/ — Estos dos son equivalentes.
  • /()?/ –o– /(x?)?/ — Estos no están obligados a participar, pero sus codiciosos ? los cuantificadores aseguran que siempre lograrán capturar al menos una cadena vacía.

Entonces, ¿cuál es la diferencia entre un NPCG y un grupo que captura una cadena vacía? Supongo que eso depende de la biblioteca de expresiones regulares, pero por lo general, a las referencias inversas a los NPCG se les asigna un valor especial nulo o indefinido.

Las siguientes son las reglas ECMA-262v3 (parafraseadas) sobre cómo deben manejarse los NPCG en JavaScript:

  • Dentro de una expresión regular, las referencias inversas a los NPCG coinciden con una cadena vacía (es decir, las referencias inversas siempre tienen éxito). Esto es desafortunado, ya que evita algunos patrones sofisticados que de otro modo serían posibles (por ejemplo, vea mi método para imitar condicionales), y es atípico en comparación con muchos otros motores de expresiones regulares, incluido Perl 5 (en el que supuestamente se basan las expresiones regulares estándar de ECMA). ), PCRE, .NET, Java, Python, Ruby, JGsoft y otros.
  • Dentro de una cadena de reemplazo, las referencias inversas a los NPCG producen una cadena vacía (es decir, nada). A diferencia del punto anterior, esto es típico en otros lugares y le permite usar una expresión regular como /a(b)|c(d)/ y reemplácelo con "$1$2" sin tener que preocuparse por punteros nulos o errores sobre grupos no participantes.
  • En las matrices de resultados de RegExp.prototype.exec , String.prototype.match (cuando se usa con una expresión regular no global), String.prototype.split y los argumentos disponibles para las funciones de devolución de llamada con String.prototype.replace , los NPCG devuelven undefined . Este es un enfoque muy lógico.

Referencias:ECMA-262v3 secciones 15.5.4.11, 15.5.4.14, 15.10.2.1, 15.10.2.3, 15.10.2.8, 15.10.2.9.

Desafortunadamente, el manejo real de los NPCG por parte del navegador está por todas partes, lo que da como resultado numerosas diferencias entre navegadores que pueden resultar fácilmente en errores sutiles (o no tan sutiles) en su código si no sabe lo que está haciendo. Por ejemplo, Firefox usa incorrectamente una cadena vacía con el replace() y split() métodos, pero usa correctamente undefined con el exec() método. Por el contrario, IE usa correctamente undefined con el replace() utiliza incorrectamente una cadena vacía con el exec() e incorrectamente no devuelve ninguno con el split() ya que no empalma las referencias inversas en la matriz resultante. En cuanto al manejo de referencias anteriores a grupos no participantes dentro expresiones regulares (por ejemplo, /(x)?\1y/.test("y") ), Safari usa el enfoque más sensato, no compatible con ECMA (devolviendo false para el bit de código anterior), mientras que IE, Firefox y Opera siguen el estándar. (Si usa /(x?)\1y/.test("y") en su lugar, los cuatro navegadores devolverán correctamente true .)

Varias veces he visto a personas encontrar estas diferencias y diagnosticarlas incorrectamente, sin haber entendido la causa raíz. Una instancia reciente es lo que motivó este artículo.

Estos son los resultados de todos los navegadores de cada uno de los métodos de expresiones regulares y de uso de expresiones regulares cuando los NPCG tienen un impacto en el resultado:

Código ECMA-262v3 IE 5.5 – 7 Firefox 2.0.0.6 Ópera 9.23 Safari 3.0.3
/(x)?\1y/.test("y") true true true true false
/(x)?\1y/.exec("y") ["y", undefined] ["y", ""] ["y", undefined] ["y", undefined] null
/(x)?y/.exec("y") ["y", undefined] ["y", ""] ["y", undefined] ["y", undefined] ["y", undefined]
"y".match(/(x)?\1y/) ["y", undefined] ["y", ""] ["y", undefined] ["y", undefined] null
"y".match(/(x)?y/) ["y", undefined] ["y", ""] ["y", undefined] ["y", undefined] ["y", undefined]
"y".match(/(x)?\1y/g) ["y"] ["y"] ["y"] ["y"] null
"y".split(/(x)?\1y/) ["", undefined, ""] [ ] ["", "", ""] ["", undefined, ""] ["y"]
"y".split(/(x)?y/) ["", undefined, ""] [ ] ["", "", ""] ["", undefined, ""] ["", ""]
"y".search(/(x)?\1y/) 0 0 0 0 -1
"y".replace(/(x)?\1y/, "z") "z" "z" "z" "z" "y"
"y".replace(/(x)?y/, "$1") "" "" "" "" ""
"y".replace(/(x)?\1y/,
    function($0, $1){
        return String($1);
    })
"undefined" "undefined" "" "undefined" "y"
"y".replace(/(x)?y/,
    function($0, $1){
        return String($1);
    })
"undefined" "undefined" "" "undefined" ""
"y".replace(/(x)?y/,
    function($0, $1){
        return $1;
    })
"undefined" "" "" "undefined" ""

(Ejecute las pruebas en su navegador).

La solución a este lío es evitar la creación de grupos de captura no participantes, a menos que sepa exactamente lo que está haciendo. Aunque eso no debería ser necesario, los NPCG suelen ser fáciles de evitar de todos modos. Vea los ejemplos cerca de la parte superior de esta publicación.

Editar (2007-08-16): He actualizado esta publicación con datos de las versiones más recientes de los navegadores enumerados. Los datos originales contenían algunos falsos negativos para Opera y Safari que resultaron de una biblioteca defectuosa utilizada para generar los resultados.