@witchcraft/expressit 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/Lexer.js +2 -2
  2. package/dist/Parser.d.ts +94 -23
  3. package/dist/Parser.d.ts.map +1 -1
  4. package/dist/Parser.js +831 -23
  5. package/dist/ast/builders/token.js +1 -1
  6. package/dist/helpers/errors.js +3 -3
  7. package/dist/helpers/parser/checkParserOpts.js +2 -2
  8. package/dist/package.json.js +3 -4
  9. package/dist/utils/extractTokens.js +1 -1
  10. package/dist/utils/getCursorInfo.js +2 -2
  11. package/dist/utils/getOppositeDelimiter.js +1 -1
  12. package/dist/utils/prettyAst.js +6 -6
  13. package/package.json +3 -4
  14. package/src/Lexer.ts +2 -2
  15. package/src/Parser.ts +1102 -60
  16. package/src/ast/builders/token.ts +1 -1
  17. package/src/helpers/errors.ts +3 -3
  18. package/src/helpers/parser/checkParserOpts.ts +2 -2
  19. package/src/utils/extractTokens.ts +1 -1
  20. package/src/utils/getCursorInfo.ts +2 -2
  21. package/src/utils/getOppositeDelimiter.ts +1 -1
  22. package/src/utils/prettyAst.ts +3 -3
  23. package/dist/methods/autocomplete.d.ts +0 -18
  24. package/dist/methods/autocomplete.d.ts.map +0 -1
  25. package/dist/methods/autocomplete.js +0 -109
  26. package/dist/methods/autoreplace.d.ts +0 -13
  27. package/dist/methods/autoreplace.d.ts.map +0 -1
  28. package/dist/methods/autoreplace.js +0 -36
  29. package/dist/methods/autosuggest.d.ts +0 -28
  30. package/dist/methods/autosuggest.d.ts.map +0 -1
  31. package/dist/methods/autosuggest.js +0 -371
  32. package/dist/methods/evaluate.d.ts +0 -11
  33. package/dist/methods/evaluate.d.ts.map +0 -1
  34. package/dist/methods/evaluate.js +0 -32
  35. package/dist/methods/getBestIndex.d.ts +0 -19
  36. package/dist/methods/getBestIndex.d.ts.map +0 -1
  37. package/dist/methods/getBestIndex.js +0 -53
  38. package/dist/methods/getIndexes.d.ts +0 -17
  39. package/dist/methods/getIndexes.d.ts.map +0 -1
  40. package/dist/methods/getIndexes.js +0 -98
  41. package/dist/methods/index.d.ts +0 -9
  42. package/dist/methods/index.d.ts.map +0 -1
  43. package/dist/methods/index.js +0 -18
  44. package/dist/methods/normalize.d.ts +0 -10
  45. package/dist/methods/normalize.d.ts.map +0 -1
  46. package/dist/methods/normalize.js +0 -98
  47. package/dist/methods/validate.d.ts +0 -11
  48. package/dist/methods/validate.d.ts.map +0 -1
  49. package/dist/methods/validate.js +0 -113
  50. package/src/methods/autocomplete.ts +0 -128
  51. package/src/methods/autoreplace.ts +0 -46
  52. package/src/methods/autosuggest.ts +0 -543
  53. package/src/methods/evaluate.ts +0 -39
  54. package/src/methods/getBestIndex.ts +0 -53
  55. package/src/methods/getIndexes.ts +0 -100
  56. package/src/methods/index.ts +0 -10
  57. package/src/methods/normalize.ts +0 -137
  58. package/src/methods/validate.ts +0 -143
package/src/Parser.ts CHANGED
@@ -1,9 +1,11 @@
1
- import { isArray } from "@alanscodelog/utils/isArray"
2
- import { isWhitespace } from "@alanscodelog/utils/isWhitespace"
3
- import { mixin } from "@alanscodelog/utils/mixin"
4
- import { setReadOnly } from "@alanscodelog/utils/setReadOnly"
5
- import type { AddParameters, Mixin } from "@alanscodelog/utils/types"
6
- import { unreachable } from "@alanscodelog/utils/unreachable"
1
+ /* eslint-disable max-lines */
2
+ import { get } from "@alanscodelog/utils/get.js"
3
+ import { insert } from "@alanscodelog/utils/insert.js"
4
+ import { isArray } from "@alanscodelog/utils/isArray.js"
5
+ import { isWhitespace } from "@alanscodelog/utils/isWhitespace.js"
6
+ import { setReadOnly } from "@alanscodelog/utils/setReadOnly.js"
7
+ import type { AddParameters , DeepPartial } from "@alanscodelog/utils/types"
8
+ import { unreachable } from "@alanscodelog/utils/unreachable.js"
7
9
 
8
10
  import { pos } from "./ast/builders/pos.js"
9
11
  import { ArrayNode } from "./ast/classes/ArrayNode.js"
@@ -11,59 +13,59 @@ import { ConditionNode } from "./ast/classes/ConditionNode.js"
11
13
  import { ErrorToken } from "./ast/classes/ErrorToken.js"
12
14
  import { ExpressionNode } from "./ast/classes/ExpressionNode.js"
13
15
  import { GroupNode } from "./ast/classes/GroupNode.js"
14
- import type { ValidToken } from "./ast/classes/ValidToken.js"
16
+ import { Condition, Expression } from "./ast/classes/index.js"
17
+ import { ValidToken } from "./ast/classes/ValidToken.js"
15
18
  import { VariableNode } from "./ast/classes/VariableNode.js"
16
19
  import * as handle from "./ast/handlers.js"
20
+ import { applyBoolean } from "./helpers/general/applyBoolean.js"
21
+ import { applyPrefix } from "./helpers/general/applyPrefix.js"
17
22
  import { checkParserOpts } from "./helpers/parser/checkParserOpts.js"
18
23
  import { extractPosition } from "./helpers/parser/extractPosition.js"
19
24
  import { getUnclosedRightParenCount } from "./helpers/parser/getUnclosedRightParenCount.js"
20
25
  import { parseParserOptions } from "./helpers/parser/parseParserOptions.js"
21
26
  import { seal } from "./helpers/parser/seal.js"
22
27
  import { $C, $T, Lexer,type RealTokenType, type Token, type TokenCategoryType, type TokenType } from "./Lexer.js"
23
- import { AutocompleteMixin } from "./methods/autocomplete.js"
24
- import { AutoreplaceMixin } from "./methods/autoreplace.js"
25
- import { Autosuggest } from "./methods/autosuggest.js"
26
- import { EvaluateMixin } from "./methods/evaluate.js"
27
- import { GetBestIndexesMixin } from "./methods/getBestIndex.js"
28
- import { GetIndexMixin } from "./methods/getIndexes.js"
29
- import { NormalizeMixin } from "./methods/normalize.js"
30
- import { ValidateMixin } from "./methods/validate.js"
31
- import type { ParserResults } from "./types/ast.js"
32
- import { type AnyToken, type Position, TOKEN_TYPE } from "./types/index.js"
33
- import type { FullParserOptions, ParserOptions } from "./types/parser.js"
34
-
35
- /**
36
- * The parser's methods are often long and have a lot of documentation per method, so it's methods have been split into mixins. They can be found in the `./methods` folder.
37
- *
38
- * Writing from within any of these methods is like writing a method from here except:
39
- * - `this` calls are wrapped in `(this as any as Parser<T>)`
40
- * - private method/property access requires `// @ts-expect-error`.
41
- * - recursion with hidden parameters requires re-typing this (see evaluate/validate for examples) since otherwise if we only retyped the function it would become unbound from `this`.
42
- *
43
- * Docs work like normal (on methods). From the outside, users of the library cannot even tell the class is composed of mixins.
44
- */
45
-
28
+ import type { ParserResults, TokenBooleanTypes } from "./types/ast.js"
29
+ import { type AnyToken, type Completion, type Position, type Suggestion,SUGGESTION_TYPE, TOKEN_TYPE } from "./types/index.js"
30
+ import type { FullParserOptions, KeywordEntry, ParserOptions, ValidationQuery, ValueQuery } from "./types/parser.js"
31
+ import { extractTokens } from "./utils/extractTokens.js"
32
+ import { getCursorInfo } from "./utils/getCursorInfo.js"
33
+ import { getSurroundingErrors } from "./utils/getSurroundingErrors.js"
34
+
35
+ const OPPOSITE = {
36
+ [TOKEN_TYPE.AND]: TOKEN_TYPE.OR,
37
+ [TOKEN_TYPE.OR]: TOKEN_TYPE.AND,
38
+ }
39
+ function isEqualSet(setA: Set<any>, setB: Set<any>): boolean {
40
+ if (setA.size !== setB.size) return false
41
+ for (const key of setA) {
42
+ if (!setB.has(key)) return false
43
+ }
44
+ return true
45
+ }
46
46
 
47
- export interface Parser<T extends {} = {}> extends Mixin<
48
- | AutocompleteMixin<T>
49
- | AutoreplaceMixin
50
- | Autosuggest<T>
51
- | EvaluateMixin<T>
52
- | ValidateMixin<T>
53
- | NormalizeMixin<T>
54
- | GetIndexMixin<T>
55
- | GetBestIndexesMixin
56
- >,
57
- AutocompleteMixin<T>,
58
- AutoreplaceMixin,
59
- Autosuggest<T>,
60
- EvaluateMixin<T>,
61
- ValidateMixin < T >,
62
- NormalizeMixin<T>,
63
- GetIndexMixin<T>,
64
- GetBestIndexesMixin
65
- {}
47
+ const defaultNodeDirs = {
48
+ before: false,
49
+ after: false,
50
+ }
66
51
 
52
+ const createDefaultRequires = (partial: DeepPartial<Suggestion["requires"]> = {}): Suggestion["requires"] => ({
53
+ whitespace: {
54
+ ...defaultNodeDirs,
55
+ ...(partial.whitespace ? partial.whitespace : {}),
56
+ },
57
+ group: partial.group ?? false,
58
+ prefix: partial.prefix ?? false,
59
+ })
60
+
61
+ /** Returns if valid token requires whitespace if none between cursor and token. */
62
+ const tokenRequiresWhitespace = (validToken: ValidToken | undefined, whitespace: boolean, wordOps: KeywordEntry[]): boolean => {
63
+ if (whitespace || validToken === undefined) return false
64
+ return validToken.type === TOKEN_TYPE.VALUE ||
65
+ ([TOKEN_TYPE.AND, TOKEN_TYPE.OR, TOKEN_TYPE.NOT].includes(validToken.type) &&
66
+ wordOps.find(_ => _.value === validToken.value) !== undefined)
67
+ }
68
+ const tokenVariable = [TOKEN_TYPE.BACKTICK, TOKEN_TYPE.DOUBLEQUOTE, TOKEN_TYPE.SINGLEQUOTE, TOKEN_TYPE.VALUE, TOKEN_TYPE.REGEX]
67
69
 
68
70
  /**
69
71
  * Creates the main parser class which handles all functionality (evaluation, validation, etc).
@@ -957,16 +959,1056 @@ export class Parser<T extends {} = {}> {
957
959
  }
958
960
  return undefined
959
961
  }
960
- }
961
962
 
962
- mixin(Parser, [
963
- AutocompleteMixin,
964
- AutoreplaceMixin,
965
- Autosuggest,
966
- EvaluateMixin,
967
- ValidateMixin,
968
- NormalizeMixin,
969
- GetIndexMixin,
970
- GetBestIndexesMixin,
971
- ])
963
+ /**
964
+ * Given a list of @see Suggestion entries, the parser options, and a list of variables, prefixes, operators, etc, and the preferred quote type, returns a list of @see Completion entries.
965
+ *
966
+ * It takes care of suggesting the correct delimiters for fixes, quoting variables/prefixes if it would not be possible to parse them unquoted, and separating symbol from non-symbol (word) operators.
967
+ *
968
+ * Does not add whitespace or group requirements. The suggestion information is still in the completion if you wish to show these. But they should not be added to the completion value if using @see autoreplace which will take care of it.
969
+ *
970
+ * Is not aware of existing values. You will have to use @see getCursorInfo to understand the context in which the suggestion was made, so that, for example, you could filter out used regex flags.
971
+ */
972
+ autocomplete(
973
+ suggestions: Suggestion[],
974
+ {
975
+ values = [], arrayValues = [], variables = [], prefixes = [], properties = [], expandedPropertyOperators = [], customPropertyOperators = (this as any as Parser<T>).options.customPropertyOperators ?? [], keywords = (this as any as Parser<T>).options.keywords, regexFlags = ["i", "m", "u"], quote = "\"",
976
+ }: Partial<Record<
977
+ "variables" |
978
+ "values" |
979
+ "arrayValues" |
980
+ "prefixes" |
981
+ "properties" |
982
+ "regexFlags" |
983
+ "expandedPropertyOperators" |
984
+ "customPropertyOperators", string[]>> & {
985
+ quote?: string
986
+ keywords?: FullParserOptions<T>["keywords"]
987
+ } = {},
988
+ ): Completion[] {
989
+ const self = (this as any as Parser<T>)
990
+ return suggestions.map(suggestion => {
991
+ const type = suggestion.type
992
+ switch (type) {
993
+ case SUGGESTION_TYPE.BACKTICK: return [{ suggestion, value: "`" }]
994
+ case SUGGESTION_TYPE.DOUBLEQUOTE: return [{ suggestion, value: "\"" }]
995
+ case SUGGESTION_TYPE.SINGLEQUOTE: return [{ suggestion, value: "'" }]
996
+ case SUGGESTION_TYPE.PARENL: return [{ suggestion, value: "(" }]
997
+ case SUGGESTION_TYPE.PARENR: return [{ suggestion, value: ")" }]
998
+ case SUGGESTION_TYPE.BRAKCETR: return [{ suggestion, value: "]" }] // L not needed
999
+ case SUGGESTION_TYPE.REGEX: return [{ suggestion, value: "/" }]
1000
+ case SUGGESTION_TYPE.REGEX_FLAGS:
1001
+ return regexFlags
1002
+ .map(value => ({ suggestion, value }))
1003
+ // remove existing flags from suggestions
1004
+ .filter(completion => {
1005
+ // eslint-disable-next-line @typescript-eslint/no-shadow
1006
+ const { suggestion, value } = completion
1007
+ if (suggestion.type !== SUGGESTION_TYPE.REGEX_FLAGS) {return true}
1008
+
1009
+ const token = suggestion.cursorInfo
1010
+ const flags = token.at && (token.at.parent as VariableNode)?.quote?.flags === suggestion.cursorInfo.at
1011
+ ? token.at
1012
+ : token.next && (token.next.parent as VariableNode)?.quote?.flags === suggestion.cursorInfo.next
1013
+ ? token.next
1014
+ : token.prev && (token.prev.parent as VariableNode)?.quote?.flags === suggestion.cursorInfo.prev
1015
+ ? token.prev
1016
+ : undefined
1017
+
1018
+ if (flags?.value?.includes(value)) {return false}
1019
+ return true
1020
+ })
1021
+ case SUGGESTION_TYPE.PROPERTY: {
1022
+ return properties.map(value => ({ suggestion, value }))
1023
+ }
1024
+ case SUGGESTION_TYPE.PROPERTY_SEP: {
1025
+ return [{ suggestion, value: self.options.expandedPropertySeparator! }]
1026
+ }
1027
+ case SUGGESTION_TYPE.EXPANDED_PROPERTY_OPERATOR: {
1028
+ return expandedPropertyOperators.map(value => ({ suggestion, value }))
1029
+ }
1030
+ case SUGGESTION_TYPE.CUSTOM_PROPERTY_OPERATOR: {
1031
+ return customPropertyOperators.map(value => ({ suggestion, value }))
1032
+ }
1033
+ case SUGGESTION_TYPE.BOOLEAN_SYMBOL_OP: {
1034
+ const keywordsList = [...keywords.and, ...keywords.or]
1035
+ const symOpts = keywordsList.filter(_ => _.isSymbol)
1036
+ return symOpts.map(({ value }) => ({ suggestion, value }))
1037
+ }
1038
+ case SUGGESTION_TYPE.BOOLEAN_WORD_OP: {
1039
+ const keywordsList = [...keywords.and, ...keywords.or]
1040
+ const wordOpts = keywordsList.filter(_ => !_.isSymbol)
1041
+ return wordOpts.map(({ value }) => ({ suggestion, value }))
1042
+ }
1043
+ case SUGGESTION_TYPE.VALUE:
1044
+ case SUGGESTION_TYPE.ARRAY_VALUE:
1045
+ case SUGGESTION_TYPE.VARIABLE: {
1046
+ const arr = type === SUGGESTION_TYPE.VARIABLE
1047
+ ? variables
1048
+ : type === SUGGESTION_TYPE.ARRAY_VALUE
1049
+ ? arrayValues
1050
+ : type === SUGGESTION_TYPE.VALUE
1051
+ ? values
1052
+ : unreachable()
1053
+ return arr.map(variable => {
1054
+ // we don't need to alter options since we can just check there are no quotes (also tells us no prefixes are used) and no operators are defined
1055
+ const res = self.parse(variable)
1056
+ if (res instanceof ConditionNode &&
1057
+ res.operator === undefined &&
1058
+ res.value instanceof VariableNode &&
1059
+ res.value.quote === undefined) {
1060
+ return { suggestion, value: res.value.value.value }
1061
+ } else {
1062
+ return { suggestion, value: quote + variable.replace(new RegExp(quote, "g"), `\\${quote}`) + quote }
1063
+ }
1064
+ })
1065
+ }
1066
+ case SUGGESTION_TYPE.PREFIX: return prefixes.map(prefix => {
1067
+ const res = self.parse(prefix)
1068
+ if (res instanceof ConditionNode &&
1069
+ res.operator === undefined &&
1070
+ res.value instanceof VariableNode &&
1071
+ res.value.quote === undefined) {
1072
+ return { suggestion, value: res.value.value.value }
1073
+ } else {
1074
+ return { suggestion, value: quote + prefix.replace(new RegExp(quote, "g"), `\\${quote}`) + quote }
1075
+ }
1076
+ })
1077
+ }
1078
+ }).flat()
1079
+ }
1080
+
1081
+ /**
1082
+ * Given the input string and a @see Completion consisting of the value of the replacement and a @see Suggestion entry, returns the replacement string and the new position of the cursor.
1083
+ *
1084
+ * The value passed should be escaped if it's needed (or quoted). @see autocomplete already takes care of quoting variables if you're using it.
1085
+ */
1086
+ autoreplace(
1087
+ input: string,
1088
+ { value, suggestion }: Completion,
1089
+ ): { replacement: string, cursor: number } {
1090
+ const isQuotedLeft = ["\"", "'", "`"].includes(value[0])
1091
+ const isQuotedRight = ["\"", "'", "`"].includes(value[value.length - 1])
1092
+ if ((isQuotedLeft && !isQuotedRight) || (!isQuotedLeft && isQuotedRight)) {
1093
+ throw new Error(`Completion value must either be entirely quoted or entirely unquoted. But the left side is ${isQuotedLeft ? "quoted" : "unquoted"} and the right side is ${isQuotedRight ? "quoted" : "unquoted"}.`)
1094
+ }
1095
+ let cursor = suggestion.range.start + value.length
1096
+
1097
+ if (suggestion.requires.prefix) {
1098
+ value = suggestion.requires.prefix + (isQuotedLeft ? "" : "\"") + value + (isQuotedRight ? "" : "\"")
1099
+
1100
+ cursor += suggestion.requires.prefix.length + Number(!isQuotedLeft) + Number(!isQuotedRight)
1101
+ }
1102
+ if (suggestion.requires.group) {
1103
+ value += "()"
1104
+ cursor++
1105
+ }
1106
+
1107
+ if (suggestion.requires.whitespace.before // &&
1108
+ ) {
1109
+ value = ` ${value}`
1110
+ cursor++
1111
+ }
1112
+ if (suggestion.requires.whitespace.after // &&
1113
+ ) {
1114
+ value = `${value} `
1115
+ }
1116
+
1117
+ const replacement = insert(value, input, [suggestion.range.start, suggestion.range.end])
1118
+ return { replacement, cursor }
1119
+ }
1120
+
1121
+ /**
1122
+ * Returns a list of suggestions ( @see Suggestion ). These are not a list of autocomplete entries (with values), but more a list of entries describing possible suggestions. This list can then be passed to @see Parser["autocomplete"] to build a list to show users, from which you can then pick an entry to pass to @see Parser["autoreplace"] .
1123
+ *
1124
+ * The list returned is "unsorted", but there is still some logic to the order. Fixes for errors are suggested first, in the order returned by @see getSurroundingErrors. Regular suggestions come after in the following order: prefixes if enabled, variables, boolean symbol operators, then boolean word operators.
1125
+ *
1126
+ * When the cursor is between two tokens that have possible suggestions, only suggestion types for the token before are returned. For example:
1127
+ *
1128
+ * ```js
1129
+ * prop="val"
1130
+ * prop|="val" //returns a property suggestions to replace `prop`
1131
+ * prop=|"val" //returns a custom operator suggestion to replace `=`
1132
+ * prop="|val" //returns a value suggestion
1133
+ * ```
1134
+ *
1135
+ * And if there are no suggestions for the previous token but there are for the next ones, they are suggested:
1136
+ * ```js
1137
+ * prop:op:"val"
1138
+ * prop:op|:"val" // returns an operator suggestion
1139
+ * prop:op:|"val" // returns a value suggestion
1140
+ * prop:op|"val" // returns a suggestion for the missing separator
1141
+ * ```
1142
+ */
1143
+ autosuggest(input: string, ast: ParserResults, index: number): Suggestion[] {
1144
+ // wrapped like this because the function is HUGE
1145
+ const opts = (this as any as Parser<T>).options
1146
+ const tokens = extractTokens(ast)
1147
+ const token = getCursorInfo(input, tokens, index)
1148
+
1149
+ const wordOps = [...opts.keywords.and, ...opts.keywords.or, ...opts.keywords.not].filter(op => !op.isSymbol)
1150
+
1151
+ const canSuggestOpAfterPrev = (
1152
+ token.valid.prev && tokenVariable.includes(token.valid.prev?.type) &&
1153
+ (token.whitespace.prev || token.valid.prev.type === TOKEN_TYPE.PARENR) &&
1154
+ !token.at && token.valid.next === undefined
1155
+ )
1156
+ const canSuggestOpBeforeNext =
1157
+ (
1158
+ token.valid.next && tokenVariable.includes(token.valid.next?.type) &&
1159
+ token.whitespace.next && // no parenL allowed since check since there will already be prefix suggestions
1160
+ !token.at && token.valid.prev === undefined
1161
+ )
1162
+
1163
+ const requiresWhitespacePrev = tokenRequiresWhitespace(token.valid.prev, token.whitespace.prev, wordOps)
1164
+ const requiresWhitespaceNext = tokenRequiresWhitespace(token.valid.next, token.whitespace.next, wordOps)
1165
+
1166
+ const requiresWhitespacePrevOp = canSuggestOpAfterPrev
1167
+ ? false
1168
+ : requiresWhitespacePrev
1169
+ const requireWhitespaceNextOp = !canSuggestOpAfterPrev && canSuggestOpBeforeNext
1170
+ ? false
1171
+ : requiresWhitespaceNext
1172
+
1173
+ const suggestions: Suggestion[] = []
1174
+ if (ast instanceof ErrorToken) {
1175
+ suggestions.push({
1176
+ type: SUGGESTION_TYPE.PREFIX,
1177
+ requires: createDefaultRequires({ group: true }),
1178
+ range: pos({ start: index }, { fill: true }),
1179
+ isError: true,
1180
+ cursorInfo: token,
1181
+ })
1182
+ suggestions.push({
1183
+ type: SUGGESTION_TYPE.VARIABLE,
1184
+ requires: createDefaultRequires(),
1185
+ range: pos({ start: index }, { fill: true }),
1186
+ isError: true,
1187
+ cursorInfo: token,
1188
+ })
1189
+ } else {
1190
+ const surroundingErrors = getSurroundingErrors(tokens, token)
1191
+
1192
+ const errorTypesHandled: TOKEN_TYPE[] = []
1193
+ const errorSuggestion = {
1194
+ isError: true,
1195
+ cursorInfo: token,
1196
+ }
1197
+ const baseSuggestion = {
1198
+ isError: false,
1199
+ cursorInfo: token,
1200
+ }
1201
+ for (const error of surroundingErrors) {
1202
+ for (const type of error.expected) {
1203
+ if (errorTypesHandled.includes(type)) continue
1204
+ errorTypesHandled.push(type)
1205
+
1206
+ switch (type) {
1207
+ case TOKEN_TYPE.DOUBLEQUOTE:
1208
+ case TOKEN_TYPE.SINGLEQUOTE:
1209
+ case TOKEN_TYPE.BACKTICK: {
1210
+ const isLeft = (error.parent as VariableNode).quote!.left === error
1211
+ const isRight = (error.parent as VariableNode).quote!.right === error
1212
+ suggestions.push({
1213
+ ...errorSuggestion,
1214
+ type: type as any as SUGGESTION_TYPE,
1215
+ requires: createDefaultRequires({
1216
+ whitespace: {
1217
+ before: isRight ? false : requiresWhitespacePrev,
1218
+ after: isLeft ? false : requiresWhitespaceNext,
1219
+ },
1220
+ }),
1221
+ range: pos({ start: index }, { fill: true }),
1222
+ })
1223
+ } break
1224
+ case TOKEN_TYPE.AND:
1225
+ case TOKEN_TYPE.OR:
1226
+ suggestions.push({
1227
+ ...errorSuggestion,
1228
+ type: SUGGESTION_TYPE.BOOLEAN_SYMBOL_OP,
1229
+ requires: createDefaultRequires(),
1230
+ range: pos({ start: index }, { fill: true }),
1231
+ })
1232
+ suggestions.push({
1233
+ ...errorSuggestion,
1234
+ type: SUGGESTION_TYPE.BOOLEAN_WORD_OP,
1235
+ requires: createDefaultRequires({
1236
+ whitespace: {
1237
+ before: requiresWhitespacePrevOp,
1238
+ after: requireWhitespaceNextOp,
1239
+ },
1240
+ }),
1241
+ range: pos({ start: index }, { fill: true }),
1242
+ })
1243
+ if (type === TOKEN_TYPE.AND) errorTypesHandled.push(TOKEN_TYPE.OR)
1244
+ if (type === TOKEN_TYPE.OR) errorTypesHandled.push(TOKEN_TYPE.AND)
1245
+
1246
+ break
1247
+ case TOKEN_TYPE.PARENL:
1248
+ case TOKEN_TYPE.PARENR:
1249
+ suggestions.push({
1250
+ ...errorSuggestion,
1251
+ type: type as any as SUGGESTION_TYPE,
1252
+ requires: createDefaultRequires(),
1253
+ range: pos({ start: index }, { fill: true }),
1254
+ })
1255
+ break
1256
+ case TOKEN_TYPE.VALUE: {
1257
+ const prefixedValue = error.parent instanceof VariableNode ? error.parent?.prefix?.value : false
1258
+ const isRegexValue = error.parent instanceof VariableNode && (
1259
+ error.parent.quote?.left.type === TOKEN_TYPE.REGEX ||
1260
+ error.parent.quote?.right.type === TOKEN_TYPE.REGEX
1261
+ )
1262
+ if (!isRegexValue) {
1263
+ // both are always suggested since missing value tokens only happen for variables
1264
+ if (!prefixedValue && opts.prefixableGroups) {
1265
+ suggestions.push({
1266
+ ...errorSuggestion,
1267
+ type: SUGGESTION_TYPE.PREFIX,
1268
+ requires: createDefaultRequires({
1269
+ whitespace: {
1270
+ before: requiresWhitespacePrev,
1271
+ after: false, /* parens get inserted */
1272
+ },
1273
+ group: true, // is always needed
1274
+ }),
1275
+ range: pos({ start: index }, { fill: true }),
1276
+ })
1277
+ }
1278
+ suggestions.push({
1279
+ ...errorSuggestion,
1280
+ type: SUGGESTION_TYPE.VARIABLE,
1281
+ requires: createDefaultRequires({
1282
+ whitespace: {
1283
+ before: requiresWhitespacePrev,
1284
+ after: requiresWhitespaceNext,
1285
+ },
1286
+ prefix: prefixedValue,
1287
+ }),
1288
+ range: pos({ start: index }, { fill: true }),
1289
+ })
1290
+ }
1291
+ break
1292
+ }
1293
+ case TOKEN_TYPE.BRACKETR: {
1294
+ suggestions.push({
1295
+ ...errorSuggestion,
1296
+ type: SUGGESTION_TYPE.BRAKCETR,
1297
+ requires: createDefaultRequires(),
1298
+ range: pos({ start: index }, { fill: true }),
1299
+ })
1300
+ break
1301
+ }
1302
+ case TOKEN_TYPE.OP_EXPANDED_SEP:
1303
+ suggestions.push({
1304
+ ...errorSuggestion,
1305
+ type: SUGGESTION_TYPE.PROPERTY_SEP,
1306
+ requires: createDefaultRequires(),
1307
+ range: pos({ start: index }, { fill: true }),
1308
+ })
1309
+ break
1310
+ case TOKEN_TYPE.REGEX:
1311
+ suggestions.push({
1312
+ ...errorSuggestion,
1313
+ type: SUGGESTION_TYPE.REGEX,
1314
+ requires: createDefaultRequires(),
1315
+ range: pos({ start: index }, { fill: true }),
1316
+ })
1317
+ break
1318
+ case TOKEN_TYPE.OP_CUSTOM:
1319
+ case TOKEN_TYPE.BRACKETL:
1320
+ case TOKEN_TYPE.NOT:
1321
+ unreachable()
1322
+ }
1323
+ }
1324
+ }
1325
+
1326
+ /** The quotes are checked because of situations like `prefix|"var"`.*/
1327
+ const prevVar = token.valid.prev?.parent
1328
+ const nextVar = token.valid.next?.parent
1329
+ const prevCondition = prevVar?.parent
1330
+ const nextCondition = nextVar?.parent
1331
+ const atVar = token.at?.parent
1332
+ const atCondition = atVar?.parent
1333
+
1334
+ const isVarPrev =
1335
+ !token.whitespace.prev &&
1336
+ token.valid.prev?.type !== TOKEN_TYPE.REGEX &&
1337
+ prevVar instanceof VariableNode &&
1338
+ (
1339
+ (
1340
+ prevCondition instanceof ConditionNode &&
1341
+ prevCondition.value === prevVar &&
1342
+ (
1343
+ prevVar.quote?.right === token.valid.prev ||
1344
+ prevVar.value === token.valid.prev
1345
+ )
1346
+ ) ||
1347
+ (
1348
+ prevCondition instanceof ArrayNode
1349
+ )
1350
+ )
1351
+
1352
+ const isVarNext =
1353
+ !token.whitespace.next &&
1354
+ token.valid.next?.type !== TOKEN_TYPE.REGEX &&
1355
+ nextVar instanceof VariableNode &&
1356
+ (
1357
+ (
1358
+ nextCondition instanceof ConditionNode &&
1359
+ nextCondition.value === nextVar &&
1360
+ (
1361
+ nextVar.quote?.left === token.valid.next ||
1362
+ nextVar.value === token.valid.next
1363
+ )
1364
+ ) ||
1365
+ (
1366
+ nextCondition instanceof ArrayNode
1367
+ )
1368
+ )
1369
+
1370
+ const isVarAt = (
1371
+ (
1372
+ atVar instanceof VariableNode &&
1373
+ atCondition instanceof ConditionNode
1374
+ ) ||
1375
+ (
1376
+ prevVar instanceof VariableNode &&
1377
+ token.valid.prev === prevVar?.quote?.left) ||
1378
+
1379
+ (
1380
+ nextVar instanceof VariableNode &&
1381
+ token.valid.next === nextVar?.quote?.right
1382
+ )
1383
+ )
1384
+
1385
+ const isPropertyPrev =
1386
+ prevCondition instanceof ConditionNode &&
1387
+ prevVar !== undefined &&
1388
+ prevVar === prevCondition?.property
1389
+ const isPropertyNext =
1390
+ nextCondition instanceof ConditionNode &&
1391
+ nextVar !== undefined &&
1392
+ nextVar === nextCondition?.property
1393
+ const isPropertyAt =
1394
+ atCondition instanceof ConditionNode &&
1395
+ atVar !== undefined &&
1396
+ atVar === atCondition?.property
1397
+
1398
+ const isPropertyOperatorPrev = prevVar instanceof ConditionNode && token.valid.prev === prevVar?.propertyOperator
1399
+ const isPropertyOperatorNext = nextVar instanceof ConditionNode && token.valid.next === nextVar?.propertyOperator
1400
+ const isPropertyOperatorAt = atVar instanceof ConditionNode && token.at === atVar?.propertyOperator
1401
+
1402
+ /** Situations like `[|]` and `[|` */
1403
+ const noArrayValuesTarget = token.valid.prev?.type === TOKEN_TYPE.BRACKETL &&
1404
+ (
1405
+ token.valid.next === undefined ||
1406
+ token.valid.next?.type === TOKEN_TYPE.BRACKETR
1407
+ )
1408
+
1409
+ /** For the following, prev tokens always have priority, next suggestions are only allowed if there are not other prev suggestions. Then lastly, only one at suggestion can exist at a time so no checks needed for those. */
1410
+ const target = isVarPrev
1411
+ ? token.valid.prev
1412
+ : !noArrayValuesTarget && !isPropertyPrev && !isPropertyOperatorPrev && isVarNext
1413
+ ? token.valid.next
1414
+ : isVarAt
1415
+ ? token.at
1416
+ : undefined
1417
+
1418
+
1419
+ const propertyTarget = isPropertyPrev
1420
+ ? token.valid.prev
1421
+ : !noArrayValuesTarget && !isVarPrev && !isPropertyOperatorPrev && isPropertyNext
1422
+ ? token.valid.next
1423
+ : isPropertyAt
1424
+ ? token.at
1425
+ : undefined
1426
+
1427
+ const propOpTarget = isPropertyOperatorPrev
1428
+ ? token.valid.prev
1429
+ : !noArrayValuesTarget && !isVarPrev && !isPropertyPrev && isPropertyOperatorNext
1430
+ ? token.valid.next
1431
+ : isPropertyOperatorAt
1432
+ ? token.at
1433
+ : undefined
1434
+
1435
+
1436
+ if (target) {
1437
+ const parent = target.parent
1438
+ if (parent instanceof VariableNode) {
1439
+ const range = pos(parent)
1440
+ const condition = parent?.parent as ConditionNode
1441
+ const isValue = condition.propertyOperator !== undefined && condition.value === parent
1442
+ const maybeGroup = parent?.parent?.parent
1443
+ const isPrefix = maybeGroup instanceof GroupNode && maybeGroup.prefix === condition
1444
+
1445
+ // look at whitespace before/after the entire variable
1446
+ const varStart = getCursorInfo(input, ast, parent.start)
1447
+ const varEnd = getCursorInfo(input, ast, parent.end)
1448
+ const targetRequiresWhitespacePrev = tokenRequiresWhitespace(varStart.valid.prev, varStart.whitespace.prev, wordOps)
1449
+ const targetRequiresWhitespaceNext = tokenRequiresWhitespace(varEnd.valid.next, varEnd.whitespace.next, wordOps)
1450
+ const prefixedValue = target.parent instanceof VariableNode ? target.parent?.prefix?.value : false
1451
+
1452
+ // most of these require additional handling below
1453
+ const isSepPrev = token.prev?.type === TOKEN_TYPE.OP_EXPANDED_SEP
1454
+ const arrayValue = target.parent?.parent instanceof ArrayNode
1455
+ const isRegexFlag = target === parent.quote?.flags
1456
+
1457
+ if (!isRegexFlag && !isSepPrev && !isValue && !arrayValue && !prefixedValue && opts.prefixableGroups) {
1458
+ suggestions.push({
1459
+ ...baseSuggestion,
1460
+ type: SUGGESTION_TYPE.PREFIX,
1461
+ requires: createDefaultRequires({
1462
+ group: !isPrefix,
1463
+ whitespace: {
1464
+ before: targetRequiresWhitespacePrev && !isPrefix,
1465
+ after: false, // parens exist or get inserted
1466
+ },
1467
+ }),
1468
+ range,
1469
+ })
1470
+ }
1471
+
1472
+ if (!isRegexFlag && !isPrefix) {
1473
+ suggestions.push({
1474
+ ...baseSuggestion,
1475
+ type: arrayValue
1476
+ ? SUGGESTION_TYPE.ARRAY_VALUE
1477
+ : isValue
1478
+ ? SUGGESTION_TYPE.VALUE
1479
+ : SUGGESTION_TYPE.VARIABLE,
1480
+ requires: createDefaultRequires({
1481
+ whitespace: {
1482
+ before: targetRequiresWhitespacePrev,
1483
+ after: targetRequiresWhitespaceNext,
1484
+ },
1485
+ prefix: prefixedValue,
1486
+ }),
1487
+ range,
1488
+ })
1489
+ }
1490
+ }
1491
+ }
1492
+
1493
+ if (noArrayValuesTarget) {
1494
+ suggestions.push({
1495
+ ...baseSuggestion,
1496
+ type: SUGGESTION_TYPE.ARRAY_VALUE,
1497
+ requires: createDefaultRequires(),
1498
+ range: pos({ start: index }, { fill: true }),
1499
+ })
1500
+ }
1501
+
1502
+ if (propertyTarget) {
1503
+ suggestions.push({
1504
+ ...baseSuggestion,
1505
+ type: SUGGESTION_TYPE.PROPERTY,
1506
+ requires: createDefaultRequires(),
1507
+ range: pos(propertyTarget),
1508
+ })
1509
+ }
1510
+ if (propOpTarget) {
1511
+ suggestions.push({
1512
+ ...baseSuggestion,
1513
+ type: (propOpTarget.parent as ConditionNode).sep
1514
+ ? SUGGESTION_TYPE.EXPANDED_PROPERTY_OPERATOR
1515
+ : SUGGESTION_TYPE.CUSTOM_PROPERTY_OPERATOR
1516
+ ,
1517
+ requires: createDefaultRequires(),
1518
+ range: pos(propOpTarget),
1519
+ })
1520
+ }
1521
+
1522
+ const canSuggestValue =
1523
+ (
1524
+ (
1525
+ token.whitespace.next &&
1526
+ (
1527
+ token.whitespace.prev ||
1528
+ token.prev?.type === TOKEN_TYPE.BRACKETL ||
1529
+ token.prev?.type === TOKEN_TYPE.PARENL
1530
+ )
1531
+ ) ||
1532
+ (
1533
+ token.whitespace.prev &&
1534
+ (
1535
+ token.whitespace.next ||
1536
+ token.next?.type === TOKEN_TYPE.BRACKETR ||
1537
+ token.next?.type === TOKEN_TYPE.PARENR
1538
+ )
1539
+ )
1540
+ )
1541
+
1542
+ if (canSuggestValue) {
1543
+ const inArrayNode = [nextCondition, prevCondition, nextVar, prevVar].find(_ => _ instanceof ArrayNode) !== undefined
1544
+ const opsNotNeeded = ["and", "or"].includes(opts.onMissingBooleanOperator)
1545
+
1546
+
1547
+ if (inArrayNode || opsNotNeeded) {
1548
+ suggestions.push({
1549
+ type: inArrayNode ? SUGGESTION_TYPE.ARRAY_VALUE : SUGGESTION_TYPE.VARIABLE,
1550
+ requires: createDefaultRequires({}),
1551
+ range: pos({ start: index }, { fill: true }),
1552
+ ...baseSuggestion,
1553
+ })
1554
+ }
1555
+ // if we're not an in array node we can also suggest prefixes
1556
+ if (!inArrayNode && opsNotNeeded) {
1557
+ suggestions.push({
1558
+ ...baseSuggestion,
1559
+ type: SUGGESTION_TYPE.PREFIX,
1560
+ requires: createDefaultRequires({
1561
+ group: true,
1562
+ }),
1563
+ range: pos({ start: index }, { fill: true }),
1564
+ })
1565
+ }
1566
+ }
1567
+
1568
+ const canSuggestRegexFlags =
1569
+ // has existing flags before/after
1570
+ (
1571
+ token.at &&
1572
+ token.at === (token.at?.parent as VariableNode)?.quote?.flags
1573
+ ) ||
1574
+ (
1575
+ token.valid.prev &&
1576
+ token.valid.prev === (token.valid.prev?.parent as VariableNode)?.quote?.flags
1577
+ ) ||
1578
+ (
1579
+ token.valid.next &&
1580
+ token.valid.next === (token.valid.next?.parent as VariableNode)?.quote?.flags
1581
+ ) ||
1582
+ ( // no flags
1583
+ token.valid.prev?.type === TOKEN_TYPE.REGEX &&
1584
+ token.valid.prev === (token.valid.prev.parent as VariableNode).quote?.right
1585
+ )
1586
+
1587
+ if (canSuggestRegexFlags) {
1588
+ suggestions.push({
1589
+ ...baseSuggestion,
1590
+ type: SUGGESTION_TYPE.REGEX_FLAGS,
1591
+ requires: createDefaultRequires(),
1592
+ range: pos({ start: index }, { fill: true }),
1593
+ })
1594
+ }
1595
+
1596
+ if (canSuggestOpAfterPrev || canSuggestOpBeforeNext) {
1597
+ const range = pos({ start: index }, { fill: true })
1598
+ suggestions.push({
1599
+ ...baseSuggestion,
1600
+ type: SUGGESTION_TYPE.BOOLEAN_SYMBOL_OP,
1601
+ requires: createDefaultRequires(),
1602
+ range,
1603
+ })
1604
+ suggestions.push({
1605
+ ...baseSuggestion,
1606
+ type: SUGGESTION_TYPE.BOOLEAN_WORD_OP,
1607
+ requires: createDefaultRequires({
1608
+ whitespace: {
1609
+ before: requiresWhitespacePrevOp,
1610
+ after: requireWhitespaceNextOp,
1611
+ },
1612
+ }),
1613
+ range,
1614
+ })
1615
+ }
1616
+ }
1617
+ return suggestions
1618
+ }
1619
+
1620
+ /**
1621
+ * Evaluates a {@link Parser.normalize normalized} ast.
1622
+ *
1623
+ * How the ast is evaluated for different operators can be controlled by the {@link ParserOptions.valueComparer valueComparer} option.
1624
+ */
1625
+ evaluate(ast: Expression<any, any> | Condition<any, any>, context: Record<string, any>): boolean {
1626
+ this._checkEvaluationOptions()
1627
+ const opts = (this as any as Parser<T>).options
1628
+
1629
+ if (ast instanceof Condition) {
1630
+ const contextValue = get(context, ast.property)
1631
+ const res = opts.valueComparer({ property: ast.property, value: ast.value, operator: ast.operator }, contextValue, context)
1632
+ return ast.negate ? !res : res
1633
+ }
1634
+ if (ast instanceof Expression) {
1635
+ const left = this.evaluate(ast.left, context)
1636
+ const right = this.evaluate(ast.right, context)
1637
+
1638
+ return ast.operator === TOKEN_TYPE.AND
1639
+ ? (left && right)
1640
+ : (left || right)
1641
+ }
1642
+
1643
+ return unreachable()
1644
+ }
1645
+
1646
+ /**
1647
+ * Given the set of indexes returned by {@link getBestIndex}, the set of existing indexes in a database, and the index to sort by\*, will return a list of the best/shortest sets of indexes.
1648
+ *
1649
+ * For example, given the query `a && b && c`, `getBestIndex` will return `[Set(a), Set(b)]`.
1650
+ *
1651
+ * Suppose we have indexes on all the variables and that the user wants to sort by `c`, this function will return [`Set(c)`].
1652
+ *
1653
+ * Suppose instead we have indexes only on `a` and `b` and that the user wants to sort by `c`, this function will return [`Set(a), Set(b)`]. Either can be picked by some other criteria (e.g. size of the indexes). Sort should then be done in memory.
1654
+ *
1655
+ * And then finally, if we have no existing indexes on any of the variables, the function will return `[]`.
1656
+ *
1657
+ * Note: This is a simple algorithm and is not designed to take into account instances where entries are indexed by two or more properties as their keys (i.e. multicolumn indexes).
1658
+ *
1659
+ * \* If the sort index is not in the list of existing indexes it is not taken into account.
1660
+ */
1661
+ getBestIndexes(indexes: Set<string>[], existing: Set<string> | Map<string, number>, sortIndex: string = ""): Set<string>[] {
1662
+ indexes = indexes.filter(set => {
1663
+ for (const key of set) {
1664
+ if (!existing.has(key)) return false
1665
+ }
1666
+ return true
1667
+ })
1668
+
1669
+ let finalIndexes = indexes
1670
+
1671
+ if (existing.has(sortIndex)) {
1672
+ const indexesWithSortIndex = indexes.filter(set => set.has(sortIndex))
1673
+ if (indexesWithSortIndex.length > 0) finalIndexes = indexesWithSortIndex
1674
+ }
1675
+
1676
+
1677
+ let smallest = Infinity
1678
+ if (existing instanceof Map) {
1679
+ const scores = new Map<Set<string>, number>()
1680
+ for (const set of finalIndexes) {
1681
+ let score = 0
1682
+ for (const key of set) {
1683
+ score += existing.get(key) ?? 0
1684
+ }
1685
+ scores.set(set, score)
1686
+ smallest = score < smallest ? score : smallest
1687
+ }
1688
+ return indexes.filter(set => smallest === Infinity || scores.get(set) === smallest)
1689
+ } else {
1690
+ for (const set of finalIndexes) {
1691
+ smallest = set.size < smallest ? set.size : smallest
1692
+ }
1693
+ return indexes.filter(set => smallest === Infinity || set.size === smallest)
1694
+ }
1695
+ }
1696
+
1697
+ /**
1698
+ * Returns a list of the different sets of keys that need to be indexed to run a normalized query on a database and hit an existing index.
1699
+ *
1700
+ * For example, the expression `a || b` requires both `a` AND `b` be indexed to use an index. The function would return `[Set(a, b)]`.
1701
+ *
1702
+ * On the otherhand, the expression `a && b` only requires `a` OR `b` to be indexed (`[Set(a), Set(b)]`) If at least one is indexed, the rest of the filtering can be done in memory. There is no need to in memory filter the entire database.
1703
+ *
1704
+ * Now take a more complicated query like `(a && b) || (a && c)`. This only requires `a` be indexed, or both `b` AND `c`. (`[Set(a)], [Set(b), Set(c)]`).
1705
+ *
1706
+ * Queries like `(a || b) && (a || c)` would require all the variables to be indexed `[Set(a), Set(b), Set(c)]`.
1707
+ */
1708
+ getIndexes(ast: Condition | Expression): Set<string>[] {
1709
+ if (ast instanceof Condition) {
1710
+ return [new Set(ast.property.join("."))]
1711
+ }
1712
+ if (ast instanceof Expression) {
1713
+ const left = this.getIndexes(ast.left)
1714
+ const right = this.getIndexes(ast.right)
1715
+
1716
+ if (ast.operator === TOKEN_TYPE.AND) {
1717
+ const sets: Set<string>[] = []
1718
+ const allKeys: Set<string> = new Set()
1719
+
1720
+ for (const leftSet of left) {
1721
+ const exists = sets.find(set => isEqualSet(set, leftSet))
1722
+ if (exists) continue
1723
+ sets.push(leftSet)
1724
+ for (const key of leftSet) {
1725
+ allKeys.add(key)
1726
+ }
1727
+ }
1728
+ for (const rightSet of right) {
1729
+ const exists = sets.find(set => isEqualSet(set, rightSet))
1730
+ if (exists) continue
1731
+ sets.push(rightSet)
1732
+ for (const key of rightSet) {
1733
+ allKeys.add(key)
1734
+ }
1735
+ }
1736
+
1737
+ const commonKeys: Set<string> = new Set()
1738
+
1739
+ // eslint-disable-next-line no-labels
1740
+ outerCheck: for (const key of allKeys) {
1741
+ for (const set of sets) {
1742
+ // eslint-disable-next-line no-labels
1743
+ if (!set.has(key)) continue outerCheck
1744
+ }
1745
+ commonKeys.add(key)
1746
+ }
1747
+ if (commonKeys.size > 0) {
1748
+ return [commonKeys, allKeys]
1749
+ } else {
1750
+ return sets
1751
+ }
1752
+ }
1753
+ if (ast.operator === TOKEN_TYPE.OR) {
1754
+ for (const rightSet of right) {
1755
+ for (const leftSet of left) {
1756
+ if (isEqualSet(leftSet, rightSet)) {
1757
+ return [rightSet]
1758
+ }
1759
+ }
1760
+ }
1761
+ const res = new Set<string>()
1762
+ for (const leftSet of left) {
1763
+ for (const key of leftSet) {
1764
+ res.add(key)
1765
+ }
1766
+ }
1767
+ for (const rightSet of right) {
1768
+ for (const key of rightSet) {
1769
+ res.add(key)
1770
+ }
1771
+ }
1772
+ return [res]
1773
+ }
1774
+ }
1775
+
1776
+ return unreachable()
1777
+ }
1778
+
1779
+ /**
1780
+ * Normalizes the ast by applying {@link GroupNode GroupNodes} and converting {@link ConditionNode ConditionNodes} to {@link NormalizedConditionNode NormalizedConditionNodes}.
1781
+ */
1782
+ normalize<TType extends string, TValue>(ast: ParserResults): Condition<TType, TValue> | Expression<TType, TValue> {
1783
+ this._checkEvaluationOptions()
1784
+ const opts = (this as any as Parser<T>).options
1785
+ if (ast instanceof ErrorToken || !ast.valid) {
1786
+ throw new Error("AST node must be valid.")
1787
+ }
1788
+ // eslint-disable-next-line prefer-rest-params
1789
+ const prefix: string | undefined = arguments[1]
1790
+ // eslint-disable-next-line prefer-rest-params
1791
+ const groupValue: boolean | undefined = arguments[2]
1792
+ // eslint-disable-next-line prefer-rest-params
1793
+ let operator: string | undefined = arguments[3]
1794
+
1795
+ const self_ = this as any as Parser & { normalize: AddParameters<Parser["normalize"], [typeof prefix, typeof groupValue, typeof operator]> }
1796
+
1797
+ if (ast instanceof ConditionNode) {
1798
+ if (!(ast.value instanceof GroupNode)) {
1799
+ const isValue = ast.value instanceof ArrayNode || (ast.value as VariableNode)?.quote?.left.type === TOKEN_TYPE.REGEX
1800
+ let name = ast.property
1801
+ ? unescape(ast.property.value.value)
1802
+ : isValue
1803
+ // the property might be missing, whether this is valid or not is up to the user
1804
+ // e.g. if prefix is defined this would make some sense
1805
+ ? undefined
1806
+ : unescape((ast.value as VariableNode)?.value.value)
1807
+ // some ancestor node went through the else block because it was a group node (e.g. prop:op(val))
1808
+ // so the "prefix" we passed is actually the name of the property (e.g. prop) and the value is the name we're getting here (e.g. val)
1809
+ const isNested = operator !== undefined
1810
+ if (prefix !== undefined && !isNested) {
1811
+ name = name ? applyPrefix(prefix, name, opts.prefixApplier) : prefix
1812
+ }
1813
+ let value: any
1814
+ if (isNested) {
1815
+ value = name ?? true
1816
+ name = prefix
1817
+ } else {
1818
+ value = ast.value instanceof ArrayNode
1819
+ ? ast.value.values.map(val => unescape(val.value.value))
1820
+ : (ast.value as VariableNode)?.quote?.left.type === TOKEN_TYPE.REGEX
1821
+ ? ast.value.value.value
1822
+ : ast.property && ast.value instanceof VariableNode
1823
+ ? unescape(ast.value.value.value)
1824
+ : true
1825
+ }
1826
+ const propertyKeys = name ? opts.keyParser(name) : []
1827
+
1828
+ const boolValue = applyBoolean(groupValue, ast.operator === undefined)
1829
+ const valuePrefix = ast.value instanceof VariableNode && ast.value.prefix
1830
+ ? unescape(ast.value.prefix.value)
1831
+ : undefined
1832
+ // one or the other might be defined, but never both since nested properties (e.g. `prop:op(prop:op(...))`) are not allowed
1833
+ operator ??= ast.propertyOperator?.value
1834
+ const isRegex = (ast.value as VariableNode)?.quote?.left.type === TOKEN_TYPE.REGEX
1835
+ const isQuoted = (ast.value as VariableNode)?.quote !== undefined
1836
+ const isExpanded = ast.sep !== undefined
1837
+ const regexFlags = (ast.value as VariableNode)?.quote?.flags?.value
1838
+ const query: ValueQuery = {
1839
+ value,
1840
+ operator,
1841
+ prefix: valuePrefix,
1842
+ regexFlags,
1843
+ property: propertyKeys,
1844
+ isRegex,
1845
+ isQuoted,
1846
+ isExpanded,
1847
+ isNegated: !boolValue,
1848
+ condition: ast,
1849
+ }
1850
+ const res = opts.conditionNormalizer(query)
1851
+ return new Condition({ property: propertyKeys, ...res })
1852
+ } else {
1853
+ let name = unescape((ast.property as VariableNode).value.value) // this is always a variable node
1854
+ if (prefix !== undefined) {
1855
+ name = applyPrefix(prefix, name, opts.prefixApplier)
1856
+ }
1857
+ const boolValue = applyBoolean(groupValue, ast.operator === undefined)
1858
+ // other operator is never defined see comments in other block above
1859
+ // eslint-disable-next-line @typescript-eslint/no-shadow
1860
+ const operator = ast.propertyOperator?.value
1861
+ // this call will at some point lead us to the above block with isNested = true
1862
+ return self_.normalize(ast.value, name, boolValue, operator) as any
1863
+ }
1864
+ }
1865
+
1866
+ if (ast instanceof GroupNode) {
1867
+ const _prefix = ast.prefix instanceof ConditionNode && ast.prefix.value instanceof VariableNode
1868
+ ? unescape(ast.prefix.value.value.value)
1869
+ : undefined // we do not want to apply not tokens
1870
+ const _groupValue = ast.prefix instanceof ConditionNode
1871
+ ? ast.prefix.operator === undefined
1872
+ : !(ast.prefix instanceof ValidToken)
1873
+
1874
+ const applied = applyPrefix(prefix, _prefix ?? "", opts.prefixApplier)
1875
+
1876
+ return self_.normalize(ast.expression as any, applied, applyBoolean(groupValue, _groupValue), operator) as any
1877
+ }
1878
+ if (ast instanceof ExpressionNode) {
1879
+ const left = self_.normalize(ast.left, prefix, groupValue, operator)
1880
+ const right = self_.normalize(ast.right, prefix, groupValue, operator)
1881
+
1882
+ // apply De Morgan's laws if group prefix was negative
1883
+ // the values are already flipped, we just have to flip the operator
1884
+ const type: TokenBooleanTypes = (groupValue === false ? OPPOSITE[ast.operator.type] : ast.operator.type) as TokenBooleanTypes
1885
+ return new Expression<TType, TValue>({ operator: type, left: left as any, right: right as any })
1886
+ }
1887
+ return unreachable()
1888
+ }
1889
+
1890
+ /**
1891
+ * Allows pre-validating ASTs for syntax highlighting purposes.
1892
+ * Works similar to evaluate. Internally it will use the prefixApplier, keyParser, and valueValidator (instead of comparer).
1893
+ *
1894
+ * The context does not need to be passed. If it's not passed, the function will not attempt to get the values (so it will not error) and the contextValue param of the valueValidator will be undefined.
1895
+ */
1896
+ validate(ast: ParserResults, context?: Record<string, any>): (Position & T)[] {
1897
+ const self = (this as any as Parser<T>)
1898
+ self._checkValidationOptions()
1899
+ const opts = self.options
1900
+ // see evaluate function, this method is practically identical, except we don't keep track of the real value (since we are not evaluating) and the actual nodes/tokens are passed to the valueValidator, not just the string values.
1901
+ if (ast instanceof ErrorToken || !ast.valid) {
1902
+ throw new Error("AST node must be valid.")
1903
+ }
1904
+ /** Handle hidden recursive version of the function. */
1905
+ // eslint-disable-next-line prefer-rest-params
1906
+ const prefix: string | undefined = arguments[2]
1907
+ // eslint-disable-next-line prefer-rest-params
1908
+ const groupValue: boolean | undefined = arguments[3]
1909
+ // eslint-disable-next-line prefer-rest-params
1910
+ const results: (Position & T)[] = arguments[4] ?? []
1911
+ // eslint-disable-next-line prefer-rest-params
1912
+ const prefixes: VariableNode[] = arguments[5] ?? []
1913
+ // eslint-disable-next-line prefer-rest-params
1914
+ let operator: ValidToken<TOKEN_TYPE.VALUE | TOKEN_TYPE.OP_CUSTOM> | undefined = arguments[6]
1915
+
1916
+ const self_ = this as any as Parser & { validate: AddParameters<Parser["validate"], [typeof prefix, typeof groupValue, typeof results, typeof prefixes, typeof operator]> }
1917
+
1918
+ if (ast instanceof ConditionNode) {
1919
+ if (!(ast.value instanceof GroupNode)) {
1920
+ const isValue = ast.value instanceof ArrayNode || (ast.value as VariableNode)?.quote?.left.type === TOKEN_TYPE.REGEX
1921
+ const nameNode = ast.property
1922
+ ? ast.property as VariableNode
1923
+ : isValue
1924
+ ? undefined
1925
+ : ast.value as VariableNode
1926
+
1927
+ let name = nameNode ? unescape(nameNode.value.value) : undefined
1928
+ const isNested = operator !== undefined
1929
+ if (prefix !== undefined && !isNested) {
1930
+ name = name ? applyPrefix(prefix, name, opts.prefixApplier) : prefix
1931
+ }
1932
+ let value: any
1933
+ let propertyNodes: VariableNode[] = []
1934
+
1935
+ if (isNested) {
1936
+ value = name
1937
+ name = prefix
1938
+ propertyNodes = [...prefixes]
1939
+ } else {
1940
+ propertyNodes = [...prefixes, ...(nameNode ? [nameNode] : [])]
1941
+ value = ast.value instanceof ArrayNode
1942
+ ? ast.value.values
1943
+ : (ast.value as VariableNode)?.quote?.left.type === TOKEN_TYPE.REGEX
1944
+ ? ast.value
1945
+ : ast.property && ast.value instanceof VariableNode
1946
+ ? ast.value
1947
+ : true
1948
+ }
1949
+ const propertyKeys = name ? opts.keyParser(name) : []
1950
+ const contextValue = context !== undefined ? get(context, propertyKeys) : undefined
1951
+
1952
+ const boolValue = applyBoolean(groupValue, ast.operator === undefined)
1953
+ const valuePrefix = ast.value instanceof VariableNode && ast.value.prefix
1954
+ ? ast.value.prefix
1955
+ : undefined
1956
+ operator ??= ast.propertyOperator as ValidToken<TOKEN_TYPE.VALUE | TOKEN_TYPE.OP_CUSTOM>
1957
+ const isRegex = (ast.value as VariableNode)?.quote?.left.type === TOKEN_TYPE.REGEX
1958
+ const isQuoted = (ast.value as VariableNode)?.quote !== undefined
1959
+ const isExpanded = ast.sep !== undefined
1960
+ const regexFlags = (ast.value as VariableNode)?.quote?.flags
1961
+ const query: ValidationQuery = {
1962
+ value,
1963
+ operator,
1964
+ prefix: valuePrefix,
1965
+ prefixes,
1966
+ property: propertyNodes,
1967
+ propertyKeys,
1968
+ propertyName: name,
1969
+ regexFlags,
1970
+ isRegex,
1971
+ isNegated: !boolValue,
1972
+ isQuoted,
1973
+ isExpanded,
1974
+ condition: ast,
1975
+ }
1976
+ const res = opts.valueValidator(contextValue, query, context)
1977
+ if (res && !isArray(res)) throw new Error("The valueValidator must return an array or nothing/undefined")
1978
+ if (res) { for (const entry of res) results.push(entry) }
1979
+ } else {
1980
+ let name = unescape((ast.property as VariableNode).value.value) // this is always a variable node
1981
+ if (prefix !== undefined) {
1982
+ name = applyPrefix(prefix, name, opts.prefixApplier)
1983
+ }
1984
+
1985
+ const boolValue = applyBoolean(groupValue, ast.operator === undefined)
1986
+
1987
+ if (ast.property) prefixes.push((ast.property as any))
1988
+ // eslint-disable-next-line @typescript-eslint/no-shadow
1989
+ const operator = ast.propertyOperator as ValidToken<TOKEN_TYPE.VALUE | TOKEN_TYPE.OP_CUSTOM>
1990
+ self_.validate(ast.value, context, name, boolValue, results, prefixes, operator)
1991
+ }
1992
+ }
1993
+
1994
+ if (ast instanceof GroupNode) {
1995
+ const _prefix = ast.prefix instanceof ConditionNode && ast.prefix.value instanceof VariableNode
1996
+ ? ast.prefix.value
1997
+ : undefined // we do not want to apply not tokens
1998
+ if (_prefix) prefixes.push(_prefix)
1999
+
2000
+ const _groupValue = ast.prefix instanceof ConditionNode
2001
+ ? ast.prefix.operator === undefined
2002
+ : !(ast.prefix instanceof ValidToken)
2003
+
2004
+ self_.validate(ast.expression as any, context, applyPrefix(prefix, _prefix?.value.value ?? "", opts.prefixApplier), applyBoolean(groupValue, _groupValue), results, prefixes, operator)
2005
+ }
2006
+ if (ast instanceof ExpressionNode) {
2007
+ // prefixes must be spread because we don't want the left branch (if it goes deeper) to affect the right
2008
+ self_.validate(ast.left, context, prefix, groupValue, results, [...prefixes], operator)
2009
+ self_.validate(ast.right, context, prefix, groupValue, results, [...prefixes], operator)
2010
+ }
2011
+ return results
2012
+ }
2013
+ }
972
2014