-
Notifications
You must be signed in to change notification settings - Fork 4
FIX @W-19772057@ Refactored selection algorithm #367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,81 +5,70 @@ export interface Selector { | |
| } | ||
|
|
||
| export function toSelector(selectorString: string): Selector { | ||
| // We parse the selector back-to-front, so that the front-most selectors end up at the bottom of the tree we create | ||
| // and therefore get resolved first. | ||
| if (selectorString === '') { | ||
| if (selectorString.includes(' ')) { | ||
| // ERROR CASE: The selector contains whitespace. | ||
| throw new Error(getMessage("SelectorLooksIncorrect", selectorString)); | ||
| } else if (selectorString === '') { | ||
| // ERROR CASE: The selector is empty. Possible if you do something like "()" or "a:()". | ||
| throw new Error(getMessage("SelectorCannotBeEmpty")); | ||
| } else if (selectorString.endsWith(')')) { | ||
| // If the selector ends in close-paren, then we need to find the open-paren that matches it. | ||
| const correspondingOpenParen: number = identifyCorrespondingOpenParen(selectorString); | ||
| if (correspondingOpenParen === 0) { | ||
| // RECURSIVE CASE: The entire selector is wrapped in parens. Pop them off and call recursively. | ||
| return toSelector(selectorString.slice(1, -1)) | ||
| } else { | ||
| // RECURSIVE CASE: The open-paren is somewhere in the middle of the selector and accompanied by an operator. | ||
| const left: string = selectorString.slice(0, correspondingOpenParen - 1); | ||
| const right: string = selectorString.slice(correspondingOpenParen); | ||
| const op: string = selectorString[correspondingOpenParen - 1]; | ||
| return toComplexSelector(left, right, op); | ||
| } | ||
| } else { | ||
| // If there's a close-paren in the string, only look for operators after it. | ||
| const lastCloseParen: number = Math.max(selectorString.lastIndexOf(')'), 0); | ||
| const lastComma: number = selectorString.slice(lastCloseParen).lastIndexOf(','); | ||
| const lastColon: number = selectorString.slice(lastCloseParen).lastIndexOf(':'); | ||
|
|
||
| // BASE CASE: The selector contains no commas or colons. | ||
| if (lastComma === -1 && lastColon === -1) { | ||
| // Parens only make sense in conjunction with operators, so if we find any, the selector is malformed. | ||
| if (selectorString.includes(')') || selectorString.includes('(')) { | ||
| throw new Error(getMessage('SelectorLooksIncorrect', selectorString)); | ||
| } | ||
| return new SimpleSelector(selectorString); | ||
| } else if (lastComma !== -1) { | ||
| // Commas resolve before colons, so that "x,a:b" and "a:b,x" both resolve equivalently the combination of | ||
| // "x" and "a:b". | ||
| const left: string = selectorString.slice(0, lastComma + lastCloseParen); | ||
| const right: string = selectorString.slice(lastComma + lastCloseParen + 1); | ||
| return toComplexSelector(left, right, ','); | ||
| } else { | ||
| const left: string = selectorString.slice(0, lastColon + lastCloseParen); | ||
| const right: string = selectorString.slice(lastColon + lastCloseParen + 1); | ||
| return toComplexSelector(left, right, ':'); | ||
| } | ||
| } else if (new RegExp('^[,:]').test(selectorString) || new RegExp('[,:]$').test(selectorString)) { | ||
| // ERROR CASE: The selector cannot start with a binary operator, because that's nonsense. | ||
| throw new Error(getMessage("SelectorStartsOrEndsWithOperator", selectorString)); | ||
|
||
| } | ||
| } | ||
|
|
||
| function identifyCorrespondingOpenParen(selectorString: string): number { | ||
| const reversedLetters: string[] = selectorString.split('').reverse(); | ||
| let commaIdx: number|null = null; | ||
| let colonIdx: number|null = null; | ||
| let parenBalance: number = 0; | ||
| let idx = 0; | ||
| for (const letter of reversedLetters) { | ||
| if (letter === ')') { | ||
| let hasParens: boolean = false; | ||
| for (let i = 0; i < selectorString.length; i++) { | ||
| const char: string = selectorString[i]; | ||
| if (char === '(') { | ||
| parenBalance += 1; | ||
| } else if (letter === '(') { | ||
| hasParens = true; | ||
| } else if (char === ')') { | ||
| parenBalance -= 1; | ||
| hasParens = true; | ||
| // If our parenthesis balance is negative, it means there are more close-parens than open-parens, which is a problem. | ||
| if (parenBalance < 0) { | ||
| throw new Error(getMessage("SelectorLooksIncorrect", selectorString)); | ||
| } | ||
| } else if (char === ',') { | ||
| // If we're not inside of parentheses, and we haven't already found a comma, note the location of this one. | ||
| if (parenBalance === 0 && commaIdx === null) { | ||
| commaIdx = i; | ||
| } | ||
| } else if (char === ':') { | ||
| // If we're not inside of parentheses, and we haven't already found a colon, note the location of this one. | ||
| if (parenBalance === 0 && colonIdx === null) { | ||
| colonIdx = i; | ||
| } | ||
|
Comment on lines
35
to
46
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of keeping track of the commaIdx, since commas trump colons, can't we just do a quick return here to avoid processing more. I only think we need to keep track of the colonIdx the whole way through. That is, can't we just do: Then you don't need lines 47-50 below. Also maybe rename colonIdx to firstColonIdx
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about that, but doing the full iteration allows us to catch situations where the number of open-parens exceeds the number of close-parens, since the balance will be positive at the end.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You will catch it anyway already because all the parens need to be balanced on both sides of the top level comma anyway and so it'll get caught in the next iteration of the loop.... so I do believe that you can indeed short circuit when you find the first top level comma. |
||
| } | ||
| if (parenBalance === 0) { | ||
| break; | ||
| } | ||
| idx += 1; | ||
| } | ||
|
|
||
| // If our final parenthesis balance is negative, it means there are more open-parens than close-parens, which is a problem. | ||
| if (parenBalance > 0) { | ||
| throw new Error(getMessage("SelectorLooksIncorrect", selectorString)); | ||
| } | ||
|
|
||
| return selectorString.length - idx - 1; | ||
| } | ||
|
|
||
| function toComplexSelector(left: string, right: string, op: string): Selector { | ||
| if (op === ',') { | ||
| // Commas trump colons, so if we have a comma, split along that. | ||
| if (commaIdx != null) { | ||
| const left: string = selectorString.slice(0, commaIdx); | ||
| const right: string = selectorString.slice(commaIdx + 1); | ||
| return new OrSelector(toSelector(left), toSelector(right)); | ||
| } else if (op === ':') { | ||
| } else if (colonIdx != null) { | ||
| // If there are colons but no commas, split along the first colon. | ||
| const left: string = selectorString.slice(0, colonIdx); | ||
| const right: string = selectorString.slice(colonIdx + 1); | ||
| return new AndSelector(toSelector(left), toSelector(right)); | ||
| } else if (selectorString[0] === '(' && selectorString[selectorString.length - 1] === ')') { | ||
| // If the first and last character are parentheses, then pop those off and run again. | ||
| return toSelector(selectorString.slice(1, selectorString.length - 1)); | ||
| } else if (hasParens) { | ||
| // There shouldn't be parentheses in the middle of a selector that has no operators. | ||
| throw new Error(getMessage('SelectorLooksIncorrect', selectorString)); | ||
| } else { | ||
| throw new Error(getMessage("SelectorLooksIncorrect", `${left}${op}${right}`)); | ||
| // A string with no operators or problems is just a simple string-selector. | ||
| return new SimpleSelector(selectorString); | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When using includes or regex testing, you are increasing the runtime since you are doing additional passes unnecessarily. We want this part of the code to be as performant as possible. So you should move the space check as in if statement inside of the loop since you are already checking character by character.