Repository: stoplightio/spectral
Pull request #251 (2019-06-07 20:45:38)

fix(pattern.ts): enable use of regex flags for pattern in ruleset files
#242

### Please check if the PR fulfills these requirements
- [x] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)

### What kind of change does this PR introduce?

Bug fix

### What is the current behavior?

See #242

### Does this PR introduce a breaking change?

None

P0lip: I went ahead and made the change. Hope you don't mind.

Patch:
diff --git a/src/functions/pattern.ts b/src/functions/pattern.ts
index ab9a2706..03a1f089 100644
--- a/src/functions/pattern.ts
+++ b/src/functions/pattern.ts
@@ -1,5 +1,25 @@
 import { IFunction, IFunctionResult, IRulePatternOptions } from '../types';
 
+function test(value: string, regex: RegExp | string) {
+  let re;
+  if (typeof regex === 'string') {
+    // regex in a string like {"match": "/[a-b]+/im"} or {"match": "[a-b]+"} in a json ruleset
+    // the available flags are "gimsuy" as described here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp
+    const splitRegex = /^\/(.+)\/([a-z]*)$/.exec(regex);
+    if (splitRegex) {
+      // with slashes like /[a-b]+/ and possibly with flags like /[a-b]+/im
+      re = new RegExp(splitRegex[1], splitRegex[2]);
+    } else {
+      // without slashes like [a-b]+
+      re = new RegExp(regex);
+    }
+  } else {
+    // true regex
+    re = new RegExp(regex);
+  }
+  return re.test(value);
+}
+
 export const pattern: IFunction<IRulePatternOptions> = (targetVal, opts) => {
   const results: IFunctionResult[] = [];
 
@@ -8,8 +28,7 @@ export const pattern: IFunction<IRulePatternOptions> = (targetVal, opts) => {
   const { match, notMatch } = opts;
 
   if (match) {
-    const re = new RegExp(match);
-    if (re.test(targetVal) !== true) {
+    if (test(targetVal, match) !== true) {
       results.push({
         message: `must match the pattern '${match}'`,
       });
@@ -17,8 +36,7 @@ export const pattern: IFunction<IRulePatternOptions> = (targetVal, opts) => {
   }
 
   if (notMatch) {
-    const re = new RegExp(notMatch);
-    if (re.test(targetVal) === true) {
+    if (test(targetVal, notMatch) === true) {
       results.push({
         message: `must not match the pattern '${notMatch}'`,
       });


Test patch:
diff --git a/src/functions/__tests__/pattern.test.ts b/src/functions/__tests__/pattern.test.ts
new file mode 100644
index 00000000..d700328d
--- /dev/null
+++ b/src/functions/__tests__/pattern.test.ts
@@ -0,0 +1,67 @@
+import { pattern } from '../pattern';
+
+function runPattern(targetVal: any, options?: any) {
+  return pattern(
+    targetVal,
+    options,
+    {
+      given: ['$'],
+    },
+    {
+      given: null,
+      original: null,
+    },
+  );
+}
+
+describe('pattern', () => {
+  describe('given a true regex', () => {
+    test('should return empty array when given value matches the given match regex', () => {
+      expect(runPattern('abc', { match: /[abc]+/ })).toEqual([]);
+    });
+
+    test('should return a message when given value does not match the given notMatch regex', () => {
+      expect(runPattern('def', { match: /[abc]+/ })).toEqual([{ message: "must match the pattern '/[abc]+/'" }]);
+    });
+
+    test('should return empty array when given value does not match the given notMatch regex', () => {
+      expect(runPattern('dEf', { notMatch: /[abc]+/i })).toEqual([]);
+    });
+
+    test('should return a message when given value does match the given notMatch regex', () => {
+      expect(runPattern('aBc', { notMatch: /[abc]+/i })).toEqual([
+        { message: "must not match the pattern '/[abc]+/i'" },
+      ]);
+    });
+  });
+
+  describe('given a string regex', () => {
+    test('should return empty array when given value matches the given match string regex without slashes', () => {
+      expect(runPattern('abc', { match: '[abc]+' })).toEqual([]);
+    });
+
+    test('should return empty array when given value matches the given match string regex with slashes', () => {
+      expect(runPattern('abc', { match: '/[abc]+/' })).toEqual([]);
+    });
+
+    test('should return empty array when given value matches the given match string regex with slashes and modifier', () => {
+      expect(runPattern('aBc', { match: '/[abc]+/im' })).toEqual([]);
+    });
+
+    test('should throw an exception when given string regex contains invalid flags', () => {
+      expect(() => runPattern('aBc', { match: '/[abc]+/invalid' })).toThrow(
+        "Invalid flags supplied to RegExp constructor 'invalid'",
+      );
+    });
+
+    test('should return empty array when given value does not match the given notMatch string regex with slashes and modifier', () => {
+      expect(runPattern('def', { notMatch: '/[abc]+/i' })).toEqual([]);
+    });
+  });
+
+  describe('given match and notMatch regexes', () => {
+    test('should return empty array when given value match the given match and does not match the given notMatch regexes', () => {
+      expect(runPattern('def', { match: /[def]+/, notMatch: /[abc]+/ })).toEqual([]);
+    });
+  });
+});