Por que um RegExp com sinalizador global fornece resultados incorretos?

277

Qual é o problema dessa expressão regular quando eu uso a bandeira global e a bandeira que não diferencia maiúsculas de minúsculas? Consulta é uma entrada gerada pelo usuário. O resultado deve ser [verdadeiro, verdadeiro].

var query = 'Foo B';
var re = new RegExp(query, 'gi');
var result = [];
result.push(re.test('Foo Bar'));
result.push(re.test('Foo Bar'));
// result will be [true, false]

var reg = /^a$/g;
for(i = 0; i++ < 10;)
   console.log(reg.test("a"));

sobre
fonte
54
Bem-vindo a uma das muitas armadilhas do RegExp em JavaScript. Ele tem uma das piores interfaces para o processamento de expressões regulares que eu já conheci, cheia de efeitos colaterais estranhos e advertências obscuras. A maioria das tarefas comuns que você normalmente deseja executar com regex é difícil de escrever corretamente.
9990
XRegExp parece uma boa alternativa. xregexp.com
sobre
Veja a resposta aqui também: stackoverflow.com/questions/604860/…
Prestaul 28/08/14
Uma solução, se você conseguir se safar, é usar o literal regex diretamente em vez de salvá-lo re.
thdoan

Respostas:

350

O RegExpobjeto controla lastIndexonde ocorreu uma correspondência, portanto, nas correspondências subsequentes, ele começará no último índice usado, em vez de 0. Dê uma olhada:

var query = 'Foo B';
var re = new RegExp(query, 'gi');
var result = [];
result.push(re.test('Foo Bar'));

alert(re.lastIndex);

result.push(re.test('Foo Bar'));

Se você não deseja redefinir manualmente lastIndexpara 0 após cada teste, basta remover o gsinalizador.

Aqui está o algoritmo que as especificações determinam (seção 15.10.6.2):

RegExp.prototype.exec (string)

Executa uma correspondência de expressão regular da cadeia de caracteres com relação à expressão regular e retorna um objeto Array que contém os resultados da correspondência, ou nulo se a cadeia de caracteres não corresponder.

  1. Seja S o valor de ToString (string).
  2. Seja comprimento o comprimento de S.
  3. Deixe lastIndex ser o valor da propriedade lastIndex.
  4. Seja eu o valor de ToInteger (lastIndex).
  5. Se a propriedade global for falsa, deixe i = 0.
  6. Se eu tiver comprimento <0 ou I>, defina lastIndex como 0 e retorne nulo.
  7. Ligue para [[Match]], fornecendo os argumentos S e i. Se [[Match]] retornou falha, vá para a etapa 8; caso contrário, r seja o resultado do Estado e vá para a etapa 10.
  8. Seja i = i + 1.
  9. Vá para o passo 6.
  10. Seja e o valor endIndex de r.
  11. Se a propriedade global for verdadeira, configure lastIndex como e.
  12. Seja n o comprimento da matriz de capturas de r. (Esse é o mesmo valor que o NCapturingParens de 15.10.2.1.)
  13. Retorne uma nova matriz com as seguintes propriedades:
    • A propriedade index é configurada para a posição da substring correspondida na cadeia completa S.
    • A propriedade de entrada está configurada para S.
    • A propriedade length está definida como n + 1.
    • A propriedade 0 é definida como a substring correspondente (ou seja, a parte de S entre o deslocamento i inclusive e o deslocamento e exclusivo).
    • Para cada número inteiro i, de modo que I> 0 e I ≤ n, defina a propriedade denominada ToString (i) como o i-ésimo elemento da matriz de capturas de r.
Ionuț G. Stan
fonte
83
Aqui é como o Guia do Mochileiro das Galáxias para o API API. "Essa armadilha em que você caiu foi perfeitamente documentada nas especificações por vários anos, se você tivesse apenas se incomodado em verificar"
Retsam
5
A bandeira pegajosa do Firefox não faz o que você implica. Em vez disso, ele age como se houvesse um ^ no início da expressão regular, EXCETO que este ^ corresponde à posição atual da string (lastIndex) em vez do início da string. Você está testando efetivamente se o regex corresponde "aqui" em vez de "em qualquer lugar após lastIndex". Veja o link que você forneceu!
Doin
1
A declaração de abertura desta resposta não é precisa. Você destacou a etapa 3 da especificação que não diz nada. A influência real de lastIndexestá nas etapas 5, 6 e 11. Sua declaração de abertura é verdadeira apenas se a bandeira global estiver configurada.
Prestaul
@ Prestaul sim, você está certo que não menciona a bandeira global. Provavelmente estava implícito (não me lembro o que eu pensava naquela época) devido ao modo como a pergunta é formulada. Sinta-se à vontade para editar a resposta ou excluí-la e vincular à sua resposta. Além disso, deixe-me garantir que você é melhor que eu. Aproveitar!
Ionuț G. Stan
@ IonuțG.Stan, desculpe se o meu comentário anterior parecia ofensivo, essa não era minha intenção. Não posso editá-lo neste momento, mas não estava tentando gritar, apenas para chamar a atenção para o ponto essencial do meu comentário. Foi mal!
Prestaul
72

Você está usando um único RegExpobjeto e o executa várias vezes. Em cada execução sucessiva, continua a partir do último índice de correspondência.

Você precisa "redefinir" a regex para iniciar do início antes de cada execução:

result.push(re.test('Foo Bar'));
re.lastIndex = 0;
result.push(re.test('Foo Bar'));
// result is now [true, true]

Dito isto, pode ser mais legível criar um novo objeto RegExp a cada vez (a sobrecarga é mínima, pois o RegExp é armazenado em cache de qualquer maneira):

result.push((/Foo B/gi).test(stringA));
result.push((/Foo B/gi).test(stringB));
Roatin Marth
fonte
1
Ou simplesmente não use a gbandeira.
Melpomene
36

RegExp.prototype.testatualiza a lastIndexpropriedade das expressões regulares para que cada teste comece onde o último parou. Eu sugiro usar, String.prototype.matchuma vez que não atualiza a lastIndexpropriedade:

!!'Foo Bar'.match(re); // -> true
!!'Foo Bar'.match(re); // -> true

Nota: !!converte para um booleano e depois inverte o booleano para refletir o resultado.

Como alternativa, você pode apenas redefinir a lastIndexpropriedade:

result.push(re.test('Foo Bar'));
re.lastIndex = 0;
result.push(re.test('Foo Bar'));
James
fonte
12

A remoção do gsinalizador global corrigirá seu problema.

var re = new RegExp(query, 'gi');

Deveria estar

var re = new RegExp(query, 'i');
user2572074
fonte
0

Você precisa definir re.lastIndex = 0 porque, com o sinalizador g, regex mantém o controle da última correspondência ocorrida, portanto, o teste não irá testar a mesma sequência, para isso você precisa fazer re.lastIndex = 0

var query = 'Foo B';
var re = new RegExp(query, 'gi');
var result = [];
result.push(re.test('Foo Bar'));
re.lastIndex=0;
result.push(re.test('Foo Bar'));

console.log(result)

Ashish
fonte
-1

Eu tinha a função:

function parseDevName(name) {
  var re = /^([^-]+)-([^-]+)-([^-]+)$/g;
  var match = re.exec(name);
  return match.slice(1,4);
}

var rv = parseDevName("BR-H-01");
rv = parseDevName("BR-H-01");

A primeira chamada funciona. A segunda ligação não. A sliceoperação reclama de um valor nulo. Presumo que isso é por causa do re.lastIndex. Isso é estranho, porque eu esperaria que um novo RegExpfosse alocado toda vez que a função for chamada e não compartilhada entre várias invocações da minha função.

Quando mudei para:

var re = new RegExp('^([^-]+)-([^-]+)-([^-]+)$', 'g');

Então eu não recebo o lastIndexefeito de ressaca. Funciona como eu esperava.

Chelmite
fonte