Skip to content

Commit

Permalink
Require literals to be marked ignoreCase if RegExps are
Browse files Browse the repository at this point in the history
  • Loading branch information
tjvr committed Feb 26, 2019
1 parent 47c216d commit f8a5814
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 13 deletions.
24 changes: 24 additions & 0 deletions moo.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
value: null,
type: null,
shouldThrow: false,
ignoreCase: null,
}

// Avoid Object.assign(), so we support IE9+
Expand Down Expand Up @@ -210,8 +211,12 @@
groups.push(options)

// Check unicode and ignoreCase flags are used everywhere or nowhere
var hasLiteralsWithCase = false
for (var j = 0; j < match.length; j++) {
var obj = match[j]
if (typeof obj === "string" && obj.toLowerCase() !== obj.toUpperCase()) {
hasLiteralsWithCase = true
}
if (!isRegExp(obj)) {
continue
}
Expand All @@ -227,6 +232,25 @@
} else if (ignoreCaseFlag !== obj.ignoreCase) {
throw new Error("If one rule is /i then all must be")
}

// RegExp flags must match the rule's ignoreCase option, if set
if (options.ignoreCase !== null && obj.ignoreCase !== options.ignoreCase) {
throw new Error("ignoreCase option must match RegExp flags (in token '" + options.defaultType + "')")
}
}

if (hasLiteralsWithCase) {
var ignoreCase = !!options.ignoreCase
if (ignoreCaseFlag === null) {
ignoreCaseFlag = ignoreCase
} else if (ignoreCaseFlag !== ignoreCase) {
if (ignoreCaseFlag) {
throw new Error("Literal must be marked with {ignoreCase: true} (in token '" + options.defaultType + "')")
} else {
// TODO transform literals to ignore case, even if it's not set globally
throw new Error("If one rule sets ignoreCase then all must (in token '" + options.defaultType + "')")
}
}
}

// convert to RegExp
Expand Down
78 changes: 65 additions & 13 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1215,27 +1215,79 @@ describe("unicode flag", () => {
describe('ignoreCase flag', () => {

test("allows all rules to be /i", () => {
expect(() => compile({ a: /foo/i, b: /bar/i, c: "quxx" })).not.toThrow()
expect(() => compile({ a: /foo/i, b: /bar/, c: "quxx" })).toThrow("If one rule is /i then all must be")
expect(() => compile({ a: /foo/, b: /bar/i, c: "quxx" })).toThrow("If one rule is /i then all must be")
expect(() => compile({ a: /foo/i, b: /bar/i })).not.toThrow()
expect(() => compile({ a: /foo/i, b: /bar/ })).toThrow("If one rule is /i then all must be")
expect(() => compile({ a: /foo/, b: /bar/i })).toThrow("If one rule is /i then all must be")
})

test("allows all rules to be /ui", () => {
expect(() => compile({ a: /foo/ui, b: /bar/ui, c: "quxx" })).not.toThrow()
expect(() => compile({ a: /foo/u, b: /bar/i, c: "quxx" })).toThrow("If one rule is /i then all must be")
expect(() => compile({ a: /foo/i, b: /bar/u, c: "quxx" })).toThrow("If one rule is /i then all must be")
expect(() => compile({ a: /foo/ui, b: /bar/i, c: "quxx" })).toThrow("If one rule is /u then all must be")
expect(() => compile({ a: /foo/ui, b: /bar/u, c: "quxx" })).toThrow("If one rule is /i then all must be")
expect(() => compile({ a: /foo/i, b: /bar/ui, c: "quxx" })).toThrow("If one rule is /u then all must be")
expect(() => compile({ a: /foo/u, b: /bar/ui, c: "quxx" })).toThrow("If one rule is /i then all must be")
expect(() => compile({ a: /foo/ui, b: /bar/ui })).not.toThrow()
expect(() => compile({ a: /foo/u, b: /bar/i })).toThrow("If one rule is /u then all must be")
expect(() => compile({ a: /foo/i, b: /bar/u })).toThrow("If one rule is /u then all must be")
expect(() => compile({ a: /foo/ui, b: /bar/i })).toThrow("If one rule is /u then all must be")
expect(() => compile({ a: /foo/ui, b: /bar/u })).toThrow("If one rule is /i then all must be")
expect(() => compile({ a: /foo/i, b: /bar/ui })).toThrow("If one rule is /u then all must be")
expect(() => compile({ a: /foo/u, b: /bar/ui })).toThrow("If one rule is /i then all must be")
})

test("allow literals to be marked ignoreCase", () => {
expect(() => compile({
a: /foo/i,
lit: {match: "quxx", ignoreCase: true},
})).not.toThrow()
expect(() => compile([
{ type: "a", match: /foo/i },
{ type: "lit", match: "quxx", ignoreCase: true },
])).not.toThrow()
})

test("require literals to be marked ignoreCase", () => {
expect(() => compile({
a: /foo/i,
lit: "quxx" ,
})).toThrow("Literal must be marked with {ignoreCase: true} (in token 'lit')")
expect(() => compile([
{ type: "a", match: /foo/i },
{ type: "lit", match: "quxx" },
])).toThrow("Literal must be marked with {ignoreCase: true} (in token 'lit')")
})

test("ignoreCase is only required when case is relevant", () => {
expect(() => compile({
cat: {match: "cat", ignoreCase: true},
bat: {match: "BAT", ignoreCase: true},
comma: ',',
semi: ';',
lparen: '(',
rparen: ')',
lbrace: '{',
rbrace: '}',
lbracket: '[',
rbracket: ']',
and: '&&',
or: '||',
bitand: '&',
bitor: '|',
})).not.toThrow()
})

test("require ignoreCase option to be match RegExp flags", () => {
expect(() => compile({
word: { match: /[a-z]+/, ignoreCase: true },
})).toThrow("ignoreCase option must match RegExp flags")
expect(() => compile({
word: { match: ["foo", /[a-z]+/], ignoreCase: true },
})).toThrow("ignoreCase option must match RegExp flags")
expect(() => compile({
word: { match: /[a-z]+/i, ignoreCase: false },
})).toThrow("ignoreCase option must match RegExp flags")
})

test("supports ignoreCase", () => {
const lexer = compile({ a: /foo/i, b: /bar/i, c: "quxx" })
lexer.reset("FoObArQuXx")
const lexer = compile({ a: /foo/i, b: /bar/i, })
lexer.reset("FoObAr")
expect(lexer.next()).toMatchObject({value: "FoO"})
expect(lexer.next()).toMatchObject({value: "bAr"})
expect(lexer.next()).toMatchObject({value: "QuXx"})
})

})

0 comments on commit f8a5814

Please sign in to comment.