Implement reference to non-existent group regex rule
MODIFY
packages/@romejs/codec-js-regexp/index.ts
packages/@romejs/codec-js-regexp/index.ts
@@ -65,6 +65,13 @@ type Tokens =
| 'S'
| 'w'
| 'W'>;
+ NumericBackReferenceCharacter: ComplexToken<
+ 'NumericBackReferenceCharacter',
+ {
+ value: number;
+ escaped: boolean;
+ }
+ >;
};
type GroupModifiers =
@@ -84,6 +91,11 @@ function isHex(str: string): boolean {
return !/[^0-9a-fA-F]/.test(str);
}
+function isOct(str: string): boolean {
+ const OCT_REGEX = /^[0-7]+$/;
+ return OCT_REGEX.test(str);
+}
+
function getCodePoint(char: string): number {
if (char.length === 1) {
const point = char.codePointAt(0);
@@ -214,7 +226,7 @@ export const createRegExpParser = createParser((ParserCore) =>
case 'x':
{
- const possibleHex = input.slice(get0(index) + 1, 2);
+ const possibleHex = input.slice(get0(index) + 1, get0(index) + 2);
// \xhh
if (possibleHex.length === 2 && isHex(possibleHex)) {
@@ -261,7 +273,46 @@ export const createRegExpParser = createParser((ParserCore) =>
default:
// TODO dangling backslash
- // TODO backreference
+ let char = nextChar;
+ let backReference = '';
+ let nextIndex: Number0 = add(index, 1);
+ while (isDigit(char)) {
+ backReference += char;
+ // stop at max octal ascii in case of octal escape
+ if (parseInt(backReference) > 377) {
+ backReference = backReference.slice(0, backReference.length - 1);
+ break;
+ }
+ nextIndex = add(nextIndex, 1);
+ char = input[get0(nextIndex)];
+ }
+
+ if (backReference !== '') {
+ const referenceValue = parseInt(backReference, 10);
+ // back reference allowed are 1 - 99
+ if (referenceValue >= 1 && referenceValue <= 99) {
+ return this.finishComplexToken(
+ 'NumericBackReferenceCharacter',
+ {
+ value: parseInt(backReference, 10),
+ escaped: true,
+ },
+ nextIndex,
+ );
+ } else {
+ backReference = backReference.slice(0, backReference.length - 1);
+ nextIndex = add(nextIndex, -1);
+ return this.finishComplexToken(
+ 'NumericBackReferenceCharacter',
+ {
+ value: parseInt(backReference, 10),
+ escaped: true,
+ },
+ nextIndex,
+ );
+ }
+ }
+
return this.finishComplexToken('Character', {
value: nextChar,
escaped: true,
@@ -482,6 +533,35 @@ export const createRegExpParser = createParser((ParserCore) =>
};
}
+ if (token.type === 'NumericBackReferenceCharacter') {
+ this.nextToken();
+ const value = token.value;
+ // \8 \9 are treated escape char
+ if (value === 8 || value === 9) {
+ return {
+ type: 'RegExpCharacter',
+ value: String(value),
+ loc: this.finishLocFromToken(token),
+ };
+ }
+
+ // octal escapes
+ if (isOct(String(value))) {
+ const octal = parseInt(String(value), 8);
+ return {
+ type: 'RegExpCharacter',
+ value: String.fromCharCode(octal),
+ loc: this.finishLocFromToken(token),
+ };
+ }
+
+ return {
+ type: 'RegExpNumericBackReference',
+ value: token.value,
+ loc: this.finishLocFromToken(token),
+ };
+ }
+
if (token.type === 'EscapedCharacter') {
this.nextToken();
@@ -755,6 +835,7 @@ export const createRegExpParser = createParser((ParserCore) =>
case 'EscapedCharacter':
case 'Character':
+ case 'NumericBackReferenceCharacter':
return this.parseCharacter();
}

MODIFY
packages/@romejs/diagnostics/categories.ts
packages/@romejs/diagnostics/categories.ts
@@ -47,6 +47,7 @@ export type DiagnosticCategory =
| 'lint/noFunctionAssign'
| 'lint/noImportAssign'
| 'lint/noLabelVar'
+ | 'lint/noReferenceToNonExistingGroup'
| 'lint/noShadowRestrictedNames'
| 'lint/noTemplateCurlyInString'
| 'lint/noUnsafeFinally'

MODIFY
packages/@romejs/diagnostics/descriptions.ts
packages/@romejs/diagnostics/descriptions.ts
@@ -404,6 +404,12 @@ export const descriptions = createMessages({
advice: buildDuplicateLocationAdvice(locations),
}),
+ NO_REFERENCE_TO_NON_EXISTING_GROUP: (name: string) =>
+ ({
+ category: 'lint/noReferenceToNonExistingGroup',
+ message: `Reference to non-existent group <emphasis>"${name}"</emphasis>`,
+ }),
+
DEFAULT_EXPORT_SAME_BASENAME: (
{defaultName, defaultType, actualFilename, correctFilename}: {
defaultName: string;

MODIFY
packages/@romejs/js-analysis/evaluators/index.ts
packages/@romejs/js-analysis/evaluators/index.ts
@@ -503,6 +503,9 @@ evaluators.set('RegExpNonWordBoundaryCharacter', RegExpNonWordBoundaryCharacter)
import RegExpNonWordCharacter from './regex/RegExpNonWordCharacter';
evaluators.set('RegExpNonWordCharacter', RegExpNonWordCharacter);
+import RegExpNumericBackReference from './regex/RegExpNumericBackReference';
+evaluators.set('RegExpNumericBackReference', RegExpNumericBackReference);
+
import RegExpQuantified from './regex/RegExpQuantified';
evaluators.set('RegExpQuantified', RegExpQuantified);

ADD
packages/@romejs/js-analysis/evaluators/regex/RegExpNumericBackReference.ts
+/**
+* Copyright (c) Facebook, Inc. and its affiliates.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+import {
+ AnyNode,
+ RegExpNumericBackReference,
+ regExpNumericBackReference,
+} from '@romejs/js-ast';
+
+export default function RegExpNumericBackReference(node: AnyNode) {
+ node = regExpNumericBackReference.assert(node);
+ throw new Error('unimplemented');
+}

MODIFY
packages/@romejs/js-ast/index.ts
packages/@romejs/js-ast/index.ts
@@ -335,6 +335,8 @@ export * from './regex/RegExpNonWordBoundaryCharacter';
export * from './regex/RegExpNonWordCharacter';
+export * from './regex/RegExpNumericBackReference';
+
export * from './regex/RegExpQuantified';
export * from './regex/RegExpStartCharacter';

ADD
packages/@romejs/js-ast/regex/RegExpNumericBackReference.ts
+/**
+* Copyright (c) Facebook, Inc. and its affiliates.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+import {JSNodeBase} from '../index';
+import {createBuilder} from '../utils';
+
+export type RegExpNumericBackReference =
+ & JSNodeBase
+ & {
+ type: 'RegExpNumericBackReference';
+ value: number;
+ };
+
+export const regExpNumericBackReference = createBuilder<
+ RegExpNumericBackReference
+>('RegExpNumericBackReference', {
+ bindingKeys: {},
+ visitorKeys: {},
+});

MODIFY
packages/@romejs/js-ast/unions.ts
packages/@romejs/js-ast/unions.ts
@@ -354,6 +354,7 @@ export type AnyRegExpEscapedCharacter =
| n.RegExpNonDigitCharacter
| n.RegExpWordBoundaryCharacter
| n.RegExpNonWordBoundaryCharacter
+ | n.RegExpNumericBackReference
| n.RegExpWhiteSpaceCharacter
| n.RegExpNonWhiteSpaceCharacter
| n.RegExpWordCharacter
@@ -546,6 +547,7 @@ export type AnyNode =
| n.RegExpNonWhiteSpaceCharacter
| n.RegExpNonWordBoundaryCharacter
| n.RegExpNonWordCharacter
+ | n.RegExpNumericBackReference
| n.RegExpQuantified
| n.RegExpStartCharacter
| n.RegExpSubExpression

MODIFY
packages/@romejs/js-compiler/transforms/lint/index.ts
packages/@romejs/js-compiler/transforms/lint/index.ts
@@ -36,6 +36,7 @@ import undeclaredVariables from './undeclaredVariables';
import unsafeNegation from './unsafeNegation';
import unusedVariables from './unusedVariables';
import noDuplicateGroupNamesInRegularExpressions from './noDuplicateGroupNamesInRegularExpressions';
+import noReferenceToNonExistingGroup from './noReferenceToNonExistingGroup';
// Add transforms in alphabetical order.
export const lintTransforms = [
@@ -59,6 +60,7 @@ export const lintTransforms = [
noFunctionAssign,
noImportAssign,
noLabelVar,
+ noReferenceToNonExistingGroup,
noShadowRestrictedNames,
noTemplateCurlyInString,
noUnsafeFinally,

ADD
packages/@romejs/js-compiler/transforms/lint/noReferenceToNonExistingGroup.test.md
+# `noReferenceToNonExistingGroup.test.ts`
+
+**DO NOT MODIFY**. This file has been autogenerated. Run `rome test packages/@romejs/js-compiler/transforms/lint/noReferenceToNonExistingGroup.test.ts --update-snapshots` to update.
+
+## `Dangling backslash in regex`
+
+### `0`
+
+```javascript
+Object {
+ src: 'let foo = /([abc]+)=\\18/;foo;'
+ suppressions: Array []
+ diagnostics: Array [
+ Object {
+ origins: Array [Object {category: 'lint'}]
+ description: Object {
+ category: 'lint/noReferenceToNonExistingGroup'
+ message: PARTIAL_BLESSED_DIAGNOSTIC_MESSAGE {value: 'Reference to non-existent group <emphasis>"18"</emphasis>'}
+ }
+ location: Object {
+ filename: 'unknown'
+ language: 'js'
+ mtime: undefined
+ sourceType: 'module'
+ end: Object {
+ column: 23
+ index: 23
+ line: 1
+ }
+ start: Object {
+ column: 20
+ index: 20
+ line: 1
+ }
+ }
+ }
+ ]
+}
+```
+
+### `1`
+
+```javascript
+Object {
+ src: 'let foo = /([abc]+)=\\49/;foo;'
+ suppressions: Array []
+ diagnostics: Array [
+ Object {
+ origins: Array [Object {category: 'lint'}]
+ description: Object {
+ category: 'lint/noReferenceToNonExistingGroup'
+ message: PARTIAL_BLESSED_DIAGNOSTIC_MESSAGE {value: 'Reference to non-existent group <emphasis>"49"</emphasis>'}
+ }
+ location: Object {
+ filename: 'unknown'
+ language: 'js'
+ mtime: undefined
+ sourceType: 'module'
+ end: Object {
+ column: 23
+ index: 23
+ line: 1
+ }
+ start: Object {
+ column: 20
+ index: 20
+ line: 1
+ }
+ }
+ }
+ ]
+}
+```
+
+### `2`
+
+```javascript
+Object {
+ src: 'let foo = /([abc]+)=\\78/;foo;'
+ suppressions: Array []
+ diagnostics: Array [
+ Object {
+ origins: Array [Object {category: 'lint'}]
+ description: Object {
+ category: 'lint/noReferenceToNonExistingGroup'
+ message: PARTIAL_BLESSED_DIAGNOSTIC_MESSAGE {value: 'Reference to non-existent group <emphasis>"78"</emphasis>'}
+ }
+ location: Object {
+ filename: 'unknown'
+ language: 'js'
+ mtime: undefined
+ sourceType: 'module'
+ end: Object {
+ column: 23
+ index: 23
+ line: 1
+ }
+ start: Object {
+ column: 20
+ index: 20
+ line: 1
+ }
+ }
+ }
+ ]
+}
+```
+
+### `3`
+
+```javascript
+Object {
+ src: 'let foo = /([abc]+)=\\99/;foo;'
+ suppressions: Array []
+ diagnostics: Array [
+ Object {
+ origins: Array [Object {category: 'lint'}]
+ description: Object {
+ category: 'lint/noReferenceToNonExistingGroup'
+ message: PARTIAL_BLESSED_DIAGNOSTIC_MESSAGE {value: 'Reference to non-existent group <emphasis>"99"</emphasis>'}
+ }
+ location: Object {
+ filename: 'unknown'
+ language: 'js'
+ mtime: undefined
+ sourceType: 'module'
+ end: Object {
+ column: 23
+ index: 23
+ line: 1
+ }
+ start: Object {
+ column: 20
+ index: 20
+ line: 1
+ }
+ }
+ }
+ ]
+}
+```
+
+### `4`
+
+```javascript
+Object {
+ src: 'let foo = /(([abc])\\19)+=\\28/;foo;'
+ suppressions: Array []
+ diagnostics: Array [
+ Object {
+ origins: Array [Object {category: 'lint'}]
+ description: Object {
+ category: 'lint/noReferenceToNonExistingGroup'
+ message: PARTIAL_BLESSED_DIAGNOSTIC_MESSAGE {value: 'Reference to non-existent group <emphasis>"19"</emphasis>'}
+ }
+ location: Object {
+ filename: 'unknown'
+ language: 'js'
+ mtime: undefined
+ sourceType: 'module'
+ end: Object {
+ column: 22
+ index: 22
+ line: 1
+ }
+ start: Object {
+ column: 19
+ index: 19
+ line: 1
+ }
+ }
+ }
+ Object {
+ origins: Array [Object {category: 'lint'}]
+ description: Object {
+ category: 'lint/noReferenceToNonExistingGroup'
+ message: PARTIAL_BLESSED_DIAGNOSTIC_MESSAGE {value: 'Reference to non-existent group <emphasis>"28"</emphasis>'}
+ }
+ location: Object {
+ filename: 'unknown'
+ language: 'js'
+ mtime: undefined
+ sourceType: 'module'
+ end: Object {
+ column: 28
+ index: 28
+ line: 1
+ }
+ start: Object {
+ column: 25
+ index: 25
+ line: 1
+ }
+ }
+ }
+ ]
+}
+```

ADD
packages/@romejs/js-compiler/transforms/lint/noReferenceToNonExistingGroup.test.ts
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import test from '@romejs/test';
+import {testLint} from '../../api/lint.test';
+
+test('Dangling backslash in regex', async (t) => {
+ const validTestCases = [
+ String.raw`let foo = /([abc]+)=\1/;foo;`, // matches first capture group
+ String.raw`let foo = /([abc]+)=\2/;foo;`, // matches \2 escaped
+ String.raw`let foo = /([abc]+)=\8/;foo;`, // matches '8'
+ String.raw`let foo = /([abc]+)=\9/;foo;`, // matches '9'
+ String.raw`let foo = /([abc]+)=\119/;foo;`, // matches '\t' followd by '9'
+ String.raw`let foo = /([abc]+)=\338/;foo;`, // matches \33 (char code 255) followed by '8'
+ String.raw`let foo = /([abc]+)=\377/;foo;`, // matches \377 (char code 255)
+ String.raw`let foo = /([abc]+)=\777/;foo;`, // matches \77 (char code 63) followed by '7'
+ ];
+
+ const invalidTestCases = [
+ String.raw`let foo = /([abc]+)=\18/;foo;`,
+ String.raw`let foo = /([abc]+)=\49/;foo;`,
+ String.raw`let foo = /([abc]+)=\78/;foo;`,
+ String.raw`let foo = /([abc]+)=\99/;foo;`,
+ String.raw`let foo = /(([abc])\19)+=\28/;foo;`,
+ ];
+
+ for (const validTestCase of validTestCases) {
+ const {diagnostics} = await testLint(validTestCase);
+ t.is(diagnostics.length, 0);
+ }
+
+ for (const invalidTestCase of invalidTestCases) {
+ let res = await testLint(invalidTestCase);
+ t.snapshot(res);
+ }
+});

ADD
packages/@romejs/js-compiler/transforms/lint/noReferenceToNonExistingGroup.ts
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import {Path} from '@romejs/js-compiler';
+import {AnyNode, RegExpGroupCapture} from '@romejs/js-ast';
+import {descriptions} from '@romejs/diagnostics';
+
+function findCaptureGroups(path: Path): Array<RegExpGroupCapture> | undefined {
+ const regexLiteral = path.findAncestry((path) =>
+ path.node.type === 'RegExpLiteral'
+ );
+ if (regexLiteral === undefined) {
+ return regexLiteral;
+ }
+ let captureGroups: Array<RegExpGroupCapture> = [];
+ regexLiteral.traverse('RegExpLiteral', (path) => {
+ if (path.node.type === 'RegExpGroupCapture') {
+ captureGroups.push(path.node);
+ }
+ });
+ return captureGroups;
+}
+
+export default {
+ name: 'noReferenceToNonExistingGroup',
+ enter(path: Path): AnyNode {
+ const {node, context} = path;
+ if (node.type === 'RegExpNumericBackReference') {
+ const allCaptureGroups = findCaptureGroups(path);
+ if (allCaptureGroups === undefined) {
+ context.addNodeDiagnostic(
+ node,
+ descriptions.LINT.NO_REFERENCE_TO_NON_EXISTING_GROUP(
+ String(node.value),
+ ),
+ );
+ } else {
+ if (node.value > allCaptureGroups.length) {
+ context.addNodeDiagnostic(
+ node,
+ descriptions.LINT.NO_REFERENCE_TO_NON_EXISTING_GROUP(String(
+ node.value,
+ )),
+ );
+ }
+ }
+ }
+ return node;
+ },
+};

MODIFY
packages/@romejs/js-generator/generators/index.ts
packages/@romejs/js-generator/generators/index.ts
@@ -497,6 +497,9 @@ generators.set('RegExpNonWordBoundaryCharacter', RegExpNonWordBoundaryCharacter)
import RegExpNonWordCharacter from './regex/RegExpNonWordCharacter';
generators.set('RegExpNonWordCharacter', RegExpNonWordCharacter);
+import RegExpNumericBackReference from './regex/RegExpNumericBackReference';
+generators.set('RegExpNumericBackReference', RegExpNumericBackReference);
+
import RegExpQuantified from './regex/RegExpQuantified';
generators.set('RegExpQuantified', RegExpQuantified);

ADD
packages/@romejs/js-generator/generators/regex/RegExpNumericBackReference.ts
+/**
+* Copyright (c) Facebook, Inc. and its affiliates.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+import Generator from '../../Generator';
+import {
+ AnyNode,
+ RegExpNumericBackReference,
+ regExpNumericBackReference,
+} from '@romejs/js-ast';
+
+export default function RegExpNumericBackReference(
+ generator: Generator,
+ node: AnyNode,
+) {
+ node = regExpNumericBackReference.assert(node);
+ return generator.append(`\\${node.value}`);
+}

Implement reference to non-existent group regex rule