expect(isMemberExpression('obj[arr[ret[bar]]]')).toBe(true)
expect(isMemberExpression('obj[arr[ret[bar]]].baz')).toBe(true)
expect(isMemberExpression('obj[1 + 1]')).toBe(true)
- // should warning
+ expect(isMemberExpression(`obj[x[0]]`)).toBe(true)
+ expect(isMemberExpression('obj[1][2]')).toBe(true)
+ expect(isMemberExpression('obj[1][2].foo[3].bar.baz')).toBe(true)
+ expect(isMemberExpression(`a[b[c.d]][0]`)).toBe(true)
+
+ // strings
+ expect(isMemberExpression(`a['foo' + bar[baz]["qux"]]`)).toBe(true)
+
+ // multiline whitespaces
+ expect(isMemberExpression('obj \n .foo \n [bar \n + baz]')).toBe(true)
+ expect(isMemberExpression(`\n model\n.\nfoo \n`)).toBe(true)
+
+ // should fail
+ expect(isMemberExpression('a \n b')).toBe(false)
expect(isMemberExpression('obj[foo')).toBe(false)
expect(isMemberExpression('objfoo]')).toBe(false)
expect(isMemberExpression('obj[arr[0]')).toBe(false)
expect(isMemberExpression('obj[arr0]]')).toBe(false)
+ expect(isMemberExpression('123[a]')).toBe(false)
+ expect(isMemberExpression('a + b')).toBe(false)
+ expect(isMemberExpression('foo()')).toBe(false)
})
export const isSimpleIdentifier = (name: string): boolean =>
!nonIdentifierRE.test(name)
-const memberExpRE = /^[A-Za-z_$\xA0-\uFFFF][\w$\xA0-\uFFFF]*(?:\s*\.\s*[A-Za-z_$\xA0-\uFFFF][\w$\xA0-\uFFFF]*|\[(.+)\])*$/
+const enum MemberExpLexState {
+ inMemberExp,
+ inBrackets,
+ inString
+}
+
+const validFirstIdentCharRE = /[A-Za-z_$\xA0-\uFFFF]/
+const validIdentCharRE = /[\.\w$\xA0-\uFFFF]/
+const whitespaceRE = /\s+[.[]\s*|\s*[.[]\s+/g
+
+/**
+ * Simple lexer to check if an expression is a member expression. This is
+ * lax and only checks validity at the root level (i.e. does not validate exps
+ * inside square brackets), but it's ok since these are only used on template
+ * expressions and false positives are invalid expressions in the first place.
+ */
export const isMemberExpression = (path: string): boolean => {
- if (!path) return false
- const matched = memberExpRE.exec(path.trim())
- if (!matched) return false
- if (!matched[1]) return true
- if (!/[\[\]]/.test(matched[1])) return true
- return isMemberExpression(matched[1].trim())
+ // remove whitespaces around . or [ first
+ path = path.trim().replace(whitespaceRE, s => s.trim())
+
+ let state = MemberExpLexState.inMemberExp
+ let prevState = MemberExpLexState.inMemberExp
+ let currentOpenBracketCount = 0
+ let currentStringType: "'" | '"' | '`' | null = null
+
+ for (let i = 0; i < path.length; i++) {
+ const char = path.charAt(i)
+ switch (state) {
+ case MemberExpLexState.inMemberExp:
+ if (char === '[') {
+ prevState = state
+ state = MemberExpLexState.inBrackets
+ currentOpenBracketCount++
+ } else if (
+ !(i === 0 ? validFirstIdentCharRE : validIdentCharRE).test(char)
+ ) {
+ return false
+ }
+ break
+ case MemberExpLexState.inBrackets:
+ if (char === `'` || char === `"` || char === '`') {
+ prevState = state
+ state = MemberExpLexState.inString
+ currentStringType = char
+ } else if (char === `[`) {
+ currentOpenBracketCount++
+ } else if (char === `]`) {
+ if (!--currentOpenBracketCount) {
+ state = prevState
+ }
+ }
+ break
+ case MemberExpLexState.inString:
+ if (char === currentStringType) {
+ state = prevState
+ currentStringType = null
+ }
+ break
+ }
+ }
+ return !currentOpenBracketCount
}
export function getInnerRange(