From 6a609ef3d4457028b110e11e1e812732a5e131a3 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:30:53 +0000 Subject: [PATCH 1/4] feat(core): Share Sentry's search grammar across the CLI and MCP Add @sentry/toolkit-core/search-syntax, which runs an unchanged copy of the Sentry frontend's searchSyntax/grammar.pegjs and builds the frontend's token tree. The structural TokenConverter skips validation and parsed values; products subclass it to add them. The parser is compiled into src/generated/ and committed, with pre-commit and check:generated keeping it in sync with the grammar. Sentry's shared fixtures/search-syntax cases are vendored and all 231 pass. The CLI's query sanitizer now parses with it, and its simplified search-query.pegjs fork and generator are gone. MCP's looksLikeSentrySearchSyntax parses with it instead of scanning for key: tokens with a regex. Co-Authored-By: Burak Yigit Kaya --- .oxfmtrc.json | 4 +- .oxlintrc.json | 2 + package.json | 2 +- packages/cli/package.json | 4 +- packages/cli/script/generate-parser.ts | 120 - packages/cli/script/search-query.pegjs | 143 - packages/cli/src/lib/search-query.ts | 225 +- packages/cli/test/lib/search-query.test.ts | 76 +- .../tools/support/search-events/utils.test.ts | 16 +- .../src/tools/support/search-events/utils.ts | 115 +- packages/toolkit-core/README.md | 27 +- .../toolkit-core/grammar/search-syntax.pegjs | 541 ++ packages/toolkit-core/package.json | 6 + .../toolkit-core/script/generate-parsers.ts | 88 + .../src/generated/search-syntax-grammar.d.ts | 27 + .../src/generated/search-syntax-grammar.js | 5522 +++++++++++++++++ .../toolkit-core/src/search-syntax.test.ts | 179 + packages/toolkit-core/src/search-syntax.ts | 1015 +++ .../aggregate_duration_filter.json | 80 + ...on_filter_overrides_numeric_shorthand.json | 41 + ...e_filter_on_explicit_typed_attributes.json | 192 + .../aggregate_rel_time_filter.json | 79 + .../search-syntax/basic_fallthrough.json | 135 + .../search-syntax/boolean_filter.json | 107 + .../boolean_filter_passthrough.json | 22 + .../search-syntax/contains_operator.json | 86 + .../search-syntax/custom_explicit_tag.json | 55 + .../fixtures/search-syntax/custom_tag.json | 72 + .../disallow_wildcard_filter.json | 194 + ...uration_aggregate_measurements_filter.json | 128 + ...ration_aggregate_op_breakdowns_filter.json | 128 + .../search-syntax/duration_filter.json | 42 + ...on_filter_overrides_numeric_shorthand.json | 23 + .../duration_measurements_filter.json | 62 + .../duration_on_non_duration_field.json | 21 + .../duration_op_breakdowns_filter.json | 62 + .../search-syntax/empty_filter_value.json | 44 + .../empty_spaces_stripped_correctly.json | 34 + .../fixtures/search-syntax/empty_string.json | 7 + .../search-syntax/ends_with_operator.json | 86 + .../search-syntax/escaped_quote_value.json | 62 + .../search-syntax/escaped_quotes.json | 40 + .../search-syntax/explicit_array_tag.json | 87 + .../search-syntax/explicit_boolean_tag.json | 55 + .../search-syntax/explicit_number_tag.json | 55 + .../explicit_number_tags_in_filter.json | 41 + .../search-syntax/explicit_string_tag.json | 55 + .../explicit_string_tags_in_filter.json | 80 + .../explicit_tags_in_filter.json | 80 + .../free_text_search_anywhere.json | 85 + .../test/fixtures/search-syntax/has_tag.json | 62 + ...aggregate_column_with_duration_filter.json | 15 + .../invalid_aggregate_duration_filter.json | 15 + .../invalid_aggregate_empty_parameter.json | 48 + .../invalid_aggregate_percentage_filter.json | 15 + .../search-syntax/invalid_boolean_filter.json | 74 + .../search-syntax/invalid_date_formats.json | 74 + .../invalid_duration_filter.json | 26 + .../invalid_numeric_aggregate_filter.json | 15 + .../search-syntax/invalid_numeric_fields.json | 122 + .../invalid_numeric_shorthand.json | 26 + .../search-syntax/multiple_quotes.json | 66 + ...egated_duration_on_non_duration_field.json | 21 + ..._boolean_values_and_non_boolean_field.json | 40 + .../search-syntax/newline_outside_quote.json | 1 + .../search-syntax/newline_within_quote.json | 21 + .../fixtures/search-syntax/not_has_tag.json | 42 + ...numeric_aggregate_measurements_filter.json | 192 + ...umeric_aggregate_op_breakdowns_filter.json | 283 + .../search-syntax/numeric_filter.json | 121 + .../numeric_filter_with_decimals.json | 24 + .../numeric_filter_with_shorthand.json | 62 + .../search-syntax/numeric_in_filter.json | 414 ++ .../numeric_measurements_filter.json | 68 + .../fixtures/search-syntax/other_dates.json | 66 + .../quoted_free_text_search_anywhere.json | 41 + .../fixtures/search-syntax/quoted_key.json | 40 + .../fixtures/search-syntax/quoted_val.json | 228 + .../search-syntax/quotes_filtered_on_raw.json | 111 + .../search-syntax/regex_empty_pattern.json | 84 + .../regex_filter_as_literal.json | 41 + .../search-syntax/regex_operator.json | 411 ++ .../search-syntax/rel_time_filter.json | 97 + .../test/fixtures/search-syntax/semver.json | 78 + .../test/fixtures/search-syntax/simple.json | 71 + .../fixtures/search-syntax/simple_in.json | 496 ++ .../search-syntax/sooo_many_quotes.json | 21 + .../special_character_params.json | 173 + .../search-syntax/specific_time_filter.json | 131 + .../search-syntax/starts_with_operator.json | 86 + .../search-syntax/supported_tags.json | 97 + .../search-syntax/tab_outside_quote.json | 24 + .../search-syntax/tab_within_quote.json | 21 + .../fixtures/search-syntax/timestamp.json | 94 + .../search-syntax/timestamp_rollup.json | 24 + .../search-syntax/trailing_quote_value.json | 120 + pnpm-lock.yaml | 6 +- scripts/pre-commit-generated.mjs | 12 + 98 files changed, 14545 insertions(+), 327 deletions(-) delete mode 100644 packages/cli/script/generate-parser.ts delete mode 100644 packages/cli/script/search-query.pegjs create mode 100644 packages/toolkit-core/grammar/search-syntax.pegjs create mode 100644 packages/toolkit-core/script/generate-parsers.ts create mode 100644 packages/toolkit-core/src/generated/search-syntax-grammar.d.ts create mode 100644 packages/toolkit-core/src/generated/search-syntax-grammar.js create mode 100644 packages/toolkit-core/src/search-syntax.test.ts create mode 100644 packages/toolkit-core/src/search-syntax.ts create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/aggregate_duration_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/aggregate_duration_filter_overrides_numeric_shorthand.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/aggregate_filter_on_explicit_typed_attributes.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/aggregate_rel_time_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/basic_fallthrough.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/boolean_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/boolean_filter_passthrough.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/contains_operator.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/custom_explicit_tag.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/custom_tag.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/disallow_wildcard_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/duration_aggregate_measurements_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/duration_aggregate_op_breakdowns_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/duration_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/duration_filter_overrides_numeric_shorthand.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/duration_measurements_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/duration_on_non_duration_field.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/duration_op_breakdowns_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/empty_filter_value.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/empty_spaces_stripped_correctly.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/empty_string.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/ends_with_operator.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/escaped_quote_value.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/escaped_quotes.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/explicit_array_tag.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/explicit_boolean_tag.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/explicit_number_tag.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/explicit_number_tags_in_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/explicit_string_tag.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/explicit_string_tags_in_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/explicit_tags_in_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/free_text_search_anywhere.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/has_tag.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_column_with_duration_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_duration_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_empty_parameter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_percentage_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/invalid_boolean_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/invalid_date_formats.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/invalid_duration_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/invalid_numeric_aggregate_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/invalid_numeric_fields.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/invalid_numeric_shorthand.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/multiple_quotes.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/negated_duration_on_non_duration_field.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/negated_on_boolean_values_and_non_boolean_field.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/newline_outside_quote.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/newline_within_quote.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/not_has_tag.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/numeric_aggregate_measurements_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/numeric_aggregate_op_breakdowns_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/numeric_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/numeric_filter_with_decimals.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/numeric_filter_with_shorthand.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/numeric_in_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/numeric_measurements_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/other_dates.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/quoted_free_text_search_anywhere.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/quoted_key.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/quoted_val.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/quotes_filtered_on_raw.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/regex_empty_pattern.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/regex_filter_as_literal.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/regex_operator.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/rel_time_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/semver.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/simple.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/simple_in.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/sooo_many_quotes.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/special_character_params.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/specific_time_filter.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/starts_with_operator.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/supported_tags.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/tab_outside_quote.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/tab_within_quote.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/timestamp.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/timestamp_rollup.json create mode 100644 packages/toolkit-core/test/fixtures/search-syntax/trailing_quote_value.json diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 50b895602..d0950ee41 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -12,6 +12,8 @@ "packages/cli/AGENTS.md", "packages/mcp-server-mocks/src/fixtures/**", "**/worker-configuration.d.ts", - "**/tsconfig*.json" + "**/tsconfig*.json", + "packages/toolkit-core/src/generated/**", + "packages/toolkit-core/test/fixtures/**" ] } diff --git a/.oxlintrc.json b/.oxlintrc.json index a1c2a6d58..b0e0c0885 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -9,6 +9,8 @@ "apps/cli-docs/**", "**/worker-configuration.d.ts", "**/tsconfig*.json", + "packages/toolkit-core/src/generated/**", + "packages/toolkit-core/test/fixtures/**", "packages/mcp-server-mocks/src/fixtures/**" ], "rules": { diff --git a/package.json b/package.json index c4e5b0971..0f63c9983 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "dev:stdio": "pnpm --filter '@sentry/mcp-server...' --if-present run build && dotenv -e .env -e .env.local -- pnpm --parallel --filter @sentry/mcp-server --filter @sentry/mcp-core --filter @sentry/mcp-server-mocks --if-present run dev", "build": "dotenv -e .env -e .env.local -- pnpm -r --filter '!sentry' --filter '!sentry-cli-docs' --if-present run build", "build:cli": "dotenv -e .env -e .env.local -- pnpm --filter sentry run build && pnpm --filter sentry-cli-docs run build", - "check:generated": "pnpm --filter @sentry/mcp-core generate-definitions && git diff --exit-code -- packages/mcp-core/src/toolDefinitions.json packages/mcp-core/src/skillDefinitions.json plugins/sentry-mcp/agents/sentry-mcp.md plugins/sentry-mcp-experimental/agents/sentry-mcp.md", + "check:generated": "pnpm --filter @sentry/mcp-core generate-definitions && pnpm --filter @sentry/toolkit-core generate && git diff --exit-code -- packages/mcp-core/src/toolDefinitions.json packages/mcp-core/src/skillDefinitions.json plugins/sentry-mcp/agents/sentry-mcp.md plugins/sentry-mcp-experimental/agents/sentry-mcp.md packages/toolkit-core/src/generated", "deploy:docs": "pnpm --filter sentry run generate:schema && pnpm --filter sentry run generate:docs && pnpm --filter sentry-cli-docs run build && pnpm --filter sentry-cli-docs run deploy", "eval": "pnpm --filter '@sentry/mcp-server-evals...' --if-present run build && dotenv -e .env -e .env.local -- pnpm --filter @sentry/mcp-server-evals run eval", "eval:ci": "pnpm --filter '@sentry/mcp-server-evals...' --if-present run build && CI=true dotenv -e .env -e .env.local -- pnpm --stream -r run eval:ci", diff --git a/packages/cli/package.json b/packages/cli/package.json index ee7e28a2f..8dab2ce3d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -69,11 +69,10 @@ "test:changed": "pnpm run generate:docs && pnpm run generate:sdk && vitest run --changed", "test:e2e": "pnpm tsx script/prepare-e2e-bundle.ts && vitest run test/e2e", "test:init-eval": "vitest run test/init-eval --testTimeout 600000", - "generate:parser": "pnpm tsx script/generate-parser.ts", "generate:sdk": "pnpm tsx script/generate-sdk.ts", "generate:skill": "pnpm tsx script/generate-skill.ts", "generate:banner": "pnpm tsx script/generate-banner-sixel.ts", - "generate:docs": "pnpm run generate:banner && pnpm run generate:parser && pnpm run generate:command-docs && pnpm run generate:skill && pnpm run generate:docs-sections", + "generate:docs": "pnpm run generate:banner && pnpm run generate:command-docs && pnpm run generate:skill && pnpm run generate:docs-sections", "generate:docs-sections": "pnpm tsx script/generate-docs-sections.ts", "generate:schema": "pnpm tsx script/generate-api-schema.ts", "generate:command-docs": "pnpm tsx script/generate-command-docs.ts", @@ -133,7 +132,6 @@ "marked": "^15.0.12", "node-sqlite3-wasm": "0.8.59", "p-limit": "^7.3.0", - "peggy": "^5.1.0", "picomatch": "^4.0.4", "pixelmatch": "^7.2.0", "pngjs": "^7.0.0", diff --git a/packages/cli/script/generate-parser.ts b/packages/cli/script/generate-parser.ts deleted file mode 100644 index b68e1603c..000000000 --- a/packages/cli/script/generate-parser.ts +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env tsx -/** - * Generate Search Query Parser from PEG Grammar - * - * Uses Peggy to compile the simplified Sentry search query grammar - * into a standalone ES module parser. The generated parser is used - * by `src/lib/search-query.ts` for structural query classification - * (OR→in-list rewriting, AND stripping, paren group handling). - * - * The grammar is a simplified version of Sentry's canonical PEG grammar: - * https://github.com/getsentry/sentry/blob/master/static/app/components/searchSyntax/grammar.pegjs - * - * Usage: - * tsx script/generate-parser.ts - * - * Output: - * src/generated/search-parser.js — the generated parser (ES module) - * src/generated/search-parser.d.ts — TypeScript type declarations - */ - -import { mkdirSync } from "node:fs"; -import { readFile, stat, writeFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; -import peggy from "peggy"; - -const GRAMMAR_PATH = resolve("script/search-query.pegjs"); -const OUTPUT_JS = resolve("src/generated/search-parser.js"); -const OUTPUT_DTS = resolve("src/generated/search-parser.d.ts"); - -const HEADER = "// Auto-generated by script/generate-parser.ts — DO NOT EDIT\n"; - -// Read the PEG grammar source -const grammarSource = await readFile(GRAMMAR_PATH, "utf-8"); - -// Compile to JavaScript source -const parserSource = peggy.generate(grammarSource, { - output: "source", - format: "es", - allowedStartRules: ["search"], -}); - -// Ensure output directory exists -mkdirSync(dirname(OUTPUT_JS), { recursive: true }); - -// Write the generated parser -await writeFile(OUTPUT_JS, `${HEADER}\n${parserSource}\n`); - -// Write TypeScript declarations -const dts = `${HEADER} -/** A text filter: key:value */ -export type TextFilter = { - type: "text_filter"; - negated: boolean; - key: string; - value: string; -}; - -/** An in-list filter: key:[val1,val2] */ -export type TextInFilter = { - type: "text_in_filter"; - negated: boolean; - key: string; - values: string[]; -}; - -export type RegexFilter = { - type: "regex_filter"; - negated: boolean; - key: string; - value: string; -}; - -/** A comparison filter: key:>value, key:<=value */ -export type ComparisonFilter = { - type: "comparison_filter"; - negated: boolean; - key: string; - op: string; - value: string; -}; - -/** Free text (no key: prefix) */ -export type FreeText = { - type: "free_text"; - value: string; - quoted: boolean; -}; - -/** A boolean operator (AND / OR) */ -export type BooleanOp = { - type: "boolean_op"; - op: "AND" | "OR"; -}; - -/** A parenthesized group */ -export type ParenGroup = { - type: "paren_group"; - inner: SearchNode[]; - raw: string; -}; - -/** Union of all search query AST node types. */ -export type SearchNode = - | TextFilter - | TextInFilter - | RegexFilter - | ComparisonFilter - | FreeText - | BooleanOp - | ParenGroup; - -/** Parse a Sentry search query into an AST node list. */ -export function parse(input: string): SearchNode[]; -`; - -await writeFile(OUTPUT_DTS, dts); - -const jsSize = ((await stat(OUTPUT_JS)).size / 1024).toFixed(1); -console.log(`Generated ${OUTPUT_JS} (${jsSize} KB)`); -console.log(`Generated ${OUTPUT_DTS}`); diff --git a/packages/cli/script/search-query.pegjs b/packages/cli/script/search-query.pegjs deleted file mode 100644 index 532eb4c10..000000000 --- a/packages/cli/script/search-query.pegjs +++ /dev/null @@ -1,143 +0,0 @@ -// Simplified Sentry search query grammar for CLI. -// -// Produces a flat list of typed AST nodes for OR→in-list rewriting. -// Derived from the canonical Sentry grammar at: -// https://github.com/getsentry/sentry/blob/master/static/app/components/searchSyntax/grammar.pegjs -// -// Key simplifications: -// - No TokenConverter or predicate-based filter type classification -// - No aggregate key/function support (not needed for rewriting) -// - No date/duration/size/percentage format parsing (classified as comparison) -// - Structural output only: text | text_in | regex | comparison | free_text | boolean_op | paren_group - -search - = _ head:term tail:(_ term)* _ { - return [head, ...tail.map(function(t) { return t[1]; })]; - } - / _ { return []; } - -term - = boolean_op / paren_group / filter / free_text - -// --------------------------------------------------------------------------- -// Boolean operators -// --------------------------------------------------------------------------- - -boolean_op - = op:("OR"i / "AND"i) &end_of_value { - return { type: "boolean_op", op: op.toUpperCase() }; - } - -// --------------------------------------------------------------------------- -// Parenthesized groups (opaque to OR rewriter) -// --------------------------------------------------------------------------- - -paren_group - = "(" _ head:term_no_paren tail:(_ term_no_paren)* _ ")" { - var inner = [head].concat(tail.map(function(t) { return t[1]; })); - return { type: "paren_group", inner: inner, raw: text() }; - } - -term_no_paren - = boolean_op / filter / free_text - -// --------------------------------------------------------------------------- -// Filters: key:value patterns -// --------------------------------------------------------------------------- - -// Order matters: try regex first (starts with //), then in-list (starts -// with [), then comparison (starts with operator), then plain text (catch-all). -filter - = negation:"!"? key:regex_filter_key ":" value:regex_value { - return { type: "regex_filter", negated: !!negation, key: key, value: value }; - } - / negation:"!"? key:filter_key ":" value:in_list { - return { type: "text_in_filter", negated: !!negation, key: key, values: value }; - } - / negation:"!"? key:filter_key ":" op:comparison_op value:filter_value { - return { type: "comparison_filter", negated: !!negation, key: key, op: op, value: value }; - } - / negation:"!"? key:filter_key ":" value:filter_value { - return { type: "text_filter", negated: !!negation, key: key, value: value }; - } - -// Keys: alphanumeric, dots, underscores, dashes, brackets (for tags[key]) -filter_key - = chars:[a-zA-Z0-9_.\[\]-]+ { return chars.join(""); } - -// Upstream accepts more key shapes before a regex value than filter_key does. -// Keep in sync with REGEX_FILTER_KEY_SOURCE in src/lib/search-query.ts. -regex_filter_key - = $(("tags" / "flags") "[" escaped_key (" "* "," " "* attribute_type)? "]" array_includes_suffix?) - / $('"' escaped_key '"' array_includes_suffix?) - / $([a-zA-Z0-9_.-]+ array_includes_suffix) - / filter_key - -escaped_key - = [a-zA-Z0-9_.:-]+ - -attribute_type - = "string" / "number" / "boolean" / "array" - -array_includes_suffix - = "[*]" - -// --------------------------------------------------------------------------- -// Values -// --------------------------------------------------------------------------- - -filter_value - = quoted_value / plain_value - -quoted_value - = '"' chars:('\\"' / [^"])* '"' { - return '"' + chars.join("") + '"'; - } - -plain_value - = chars:[^ \t\n()\[\],]+ { return chars.join(""); } - -regex_value - = $("//" (!("//" end_of_value) [^\n])+ "//" &end_of_value) - -// In-list: [val1,val2,"val 3"] -in_list - = "[" _ head:in_list_item tail:(_ "," _ in_list_item)* _ "]" { - return [head].concat(tail.map(function(t) { return t[3]; })); - } - -in_list_item - = quoted_value / in_list_plain - -in_list_plain - = chars:[^,\] \t\n"]+ { return chars.join(""); } - -// Comparison operators (order matters: >= before >, etc.) -comparison_op - = ">=" / "<=" / ">" / "<" / "=" / "!=" - -// --------------------------------------------------------------------------- -// Free text (anything that isn't a filter or boolean operator) -// -// Consumes whole words (matching upstream grammar's free_text_unquoted). -// The negative lookaheads check the entire remaining text, not individual -// characters, preventing false matches on substrings like "or" in "error". -// --------------------------------------------------------------------------- - -free_text - = value:quoted_value { - return { type: "free_text", value: value, quoted: true }; - } - / !filter !boolean_op value:$[^ \t\n()]+ { - return { type: "free_text", value: value, quoted: false }; - } - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -_ "whitespace" - = [ \t]* - -end_of_value - = [ \t\n)] / !. diff --git a/packages/cli/src/lib/search-query.ts b/packages/cli/src/lib/search-query.ts index 5bd734711..1d1ee2999 100644 --- a/packages/cli/src/lib/search-query.ts +++ b/packages/cli/src/lib/search-query.ts @@ -15,17 +15,22 @@ * with a warning. Slugs, `project_id:…`, and namespaced keys * (`bolt.project_id`) are left alone. * - * Parsing uses a pre-compiled PEG parser generated from - * `script/search-query.pegjs` (a simplified version of Sentry's - * canonical grammar). The parser classifies every term structurally — - * comparison operators, in-list values, paren groups, etc. — so the - * rewriting logic doesn't need regex-based reject-lists. + * Parsing uses the Sentry frontend's search grammar through + * `@sentry/toolkit-core/search-syntax`. Its token tree is flattened into + * the {@link SearchNode} list below, which classifies every term + * structurally — comparison operators, in-list values, paren groups, + * etc. — so the rewriting logic doesn't need regex-based reject-lists. * * @see {@link https://github.com/getsentry/sentry/blob/master/static/app/components/searchSyntax/grammar.pegjs | Canonical Sentry PEG grammar} */ -import type { SearchNode } from "../generated/search-parser.js"; -import { parse } from "../generated/search-parser.js"; +import { + type FilterToken, + type ParseResultToken, + parseSearch, + TermOperator, + Token, +} from "@sentry/toolkit-core/search-syntax"; import { ValidationError } from "./errors.js"; import { logger } from "./logger.js"; @@ -39,6 +44,198 @@ const log = logger.withTag("search-query"); */ const INVALID_INLIST_KEYS = new Set(["is", "has"]); +// --------------------------------------------------------------------------- +// Search nodes +// --------------------------------------------------------------------------- + +/** A text filter: key:value */ +type TextFilter = { + type: "text_filter"; + negated: boolean; + key: string; + value: string; +}; + +/** An in-list filter: key:[val1,val2] */ +type TextInFilter = { + type: "text_in_filter"; + negated: boolean; + key: string; + values: string[]; +}; + +/** A regex filter: key://pattern// */ +type RegexFilter = { + type: "regex_filter"; + negated: boolean; + key: string; + value: string; +}; + +/** A comparison filter: key:>value, key:<=value */ +type ComparisonFilter = { + type: "comparison_filter"; + negated: boolean; + key: string; + op: string; + value: string; +}; + +/** Free text (no key: prefix) */ +type FreeText = { + type: "free_text"; + value: string; + quoted: boolean; +}; + +/** A boolean operator (AND / OR) */ +type BooleanOp = { + type: "boolean_op"; + op: "AND" | "OR"; +}; + +/** A parenthesized group */ +type ParenGroup = { + type: "paren_group"; + inner: SearchNode[]; + raw: string; +}; + +/** + * A filter the rewriter must pass through untouched, such as an empty value + * or a list with an empty item. + */ +type OpaqueTerm = { + type: "opaque"; + raw: string; +}; + +/** One term of a search query. */ +export type SearchNode = + | TextFilter + | TextInFilter + | RegexFilter + | ComparisonFilter + | FreeText + | BooleanOp + | ParenGroup + | OpaqueTerm; + +const COMPARISON_PREFIX_RE = /^(>=|<=|!=|>|<|=)/; +const FREE_TEXT_SEPARATOR_RE = /[ \t]+/; + +type Located = { + location: { start: { offset: number }; end: { offset: number } }; +}; + +/** The exact query text a token was parsed from. */ +function sourceOf(query: string, token: Located): string { + return query.slice(token.location.start.offset, token.location.end.offset); +} + +/** Flatten one filter token into a {@link SearchNode}. */ +function filterToNode(query: string, token: FilterToken): SearchNode { + const key = sourceOf(query, token.key); + // Everything after the `:` that follows the key, operator included. + const value = query.slice( + token.key.location.end.offset + 1, + token.location.end.offset, + ); + const { negated } = token; + + if (token.operator === TermOperator.MATCHES) { + return { type: "regex_filter", negated, key, value }; + } + + if ( + token.value.type === Token.VALUE_TEXT_LIST || + token.value.type === Token.VALUE_NUMBER_LIST + ) { + const values: string[] = []; + for (const item of token.value.items) { + if (!item.value || token.operator !== TermOperator.DEFAULT) { + return { type: "opaque", raw: sourceOf(query, token) }; + } + values.push(sourceOf(query, item.value)); + } + return { type: "text_in_filter", negated, key, values }; + } + + const comparison = COMPARISON_PREFIX_RE.exec(value)?.[1]; + if (comparison) { + return { + type: "comparison_filter", + negated, + key, + op: comparison, + value: value.slice(comparison.length), + }; + } + + if (!value) { + return { type: "opaque", raw: sourceOf(query, token) }; + } + + return { type: "text_filter", negated, key, value }; +} + +/** + * Flatten the shared parser's token tree into {@link SearchNode}s. Returns + * `null` for unbalanced parentheses, which the rewriter can't reason about. + */ +function toSearchNodes( + query: string, + tokens: ParseResultToken[], +): SearchNode[] | null { + const nodes: SearchNode[] = []; + for (const token of tokens) { + switch (token.type) { + case Token.SPACES: + break; + case Token.LOGIC_BOOLEAN: + nodes.push({ type: "boolean_op", op: token.value }); + break; + case Token.LOGIC_GROUP: { + const inner = toSearchNodes(query, token.inner); + if (!inner) { + return null; + } + nodes.push({ type: "paren_group", inner, raw: sourceOf(query, token) }); + break; + } + case Token.FREE_TEXT: { + const text = sourceOf(query, token).trim(); + if (token.quoted) { + nodes.push({ type: "free_text", value: text, quoted: true }); + break; + } + // The grammar folds consecutive words into one free-text token; the + // rewriter treats each word as its own term. + for (const word of text.split(FREE_TEXT_SEPARATOR_RE)) { + if (word) { + nodes.push({ type: "free_text", value: word, quoted: false }); + } + } + break; + } + case Token.FILTER: + nodes.push(filterToNode(query, token)); + break; + default: + return null; + } + } + return nodes; +} + +/** Parse a query into {@link SearchNode}s, or `null` if it can't be. */ +function parseSearchNodes(query: string): SearchNode[] | null { + // Regex filters are parsed as such on every dataset so the rewriter never + // merges a `key://pattern//` value into an in-list. + const tokens = parseSearch(query, { config: { allowRegex: true } }); + return tokens ? toSearchNodes(query, tokens) : null; +} + // --------------------------------------------------------------------------- // AST → string serialization // --------------------------------------------------------------------------- @@ -71,6 +268,9 @@ function serializeNode(node: SearchNode): string { const prefix = node.negated ? "!" : ""; return `${prefix}${node.key}:${node.op}${node.value}`; } + case "opaque": { + return node.raw; + } default: { return ""; } @@ -357,14 +557,12 @@ export function sanitizeQuery(query: string | undefined): string | undefined { const withNumericProject = rewriteNumericProjectFilters(normalized); const notes = preParseRewriteNotes(query, normalized, withNumericProject); - let nodes: SearchNode[]; - try { - nodes = parse(withNumericProject); - } catch (err) { + const nodes = parseSearchNodes(withNumericProject); + if (!nodes) { // PEG parse still failed after normalization — pass through to the // API which returns a proper 400 with actionable details. The text // rewrites already ran, so say so: the 400 will quote them. - log.debug("Search query did not parse; sending as-is", err); + log.debug("Search query did not parse; sending as-is"); warnRunningQuery(notes, withNumericProject); return withNumericProject; } @@ -570,7 +768,7 @@ const PROJECT_NUMERIC_LIST_RE = /(^|\s)(!?)project:\[(\d+(?:\s*,\s*\d+)*)\]/gi; * whitespace, `)`, or end of query. Between matches is unquoted text that * can be safely normalized. */ -const REGEX_FILTER_KEY_SOURCE = String.raw`(?:(?:tags|flags)\[[\w.:-]+(?: *, *(?:string|number|boolean|array))?\]|"[\w.:-]+"|[\w.[\]-]+)(?:\[\*\])?`; +const REGEX_FILTER_KEY_SOURCE = String.raw`(?:(?:tags|flags)\[[\w.:-]+(?:\[\*\])?(?: *, *(?:string|number|boolean|array))?\]|"[\w.:-]+"|[\w.[\]-]+)(?:\[\*\])?`; const PRESERVED_SEGMENT_RE = new RegExp( String.raw`"(?:[^"\\]|\\.)*"|(?<=(?:^|[\s()"\]])!?${REGEX_FILTER_KEY_SOURCE}:)\/\/(?:(?!\/\/(?:[\t\n )]|$))[^\n])+\/\/(?=[\t\n )]|$)`, "g", @@ -695,6 +893,7 @@ function stripTrailingListCommas(query: string): string { * @internal Exported for testing only. Not part of the public API. */ export const __testing = { + parseSearchNodes, isMergeableFilter, tryMergeGroup, tryRewriteOr, diff --git a/packages/cli/test/lib/search-query.test.ts b/packages/cli/test/lib/search-query.test.ts index 5dc9e9a86..f42acfc45 100644 --- a/packages/cli/test/lib/search-query.test.ts +++ b/packages/cli/test/lib/search-query.test.ts @@ -14,7 +14,7 @@ import { describe, expect, test } from "vitest"; import { ValidationError } from "../../src/lib/errors.js"; import { __testing, sanitizeQuery } from "../../src/lib/search-query.js"; -const { normalizeQuery, transformUnquoted } = __testing; +const { normalizeQuery, parseSearchNodes, transformUnquoted } = __testing; // --------------------------------------------------------------------------- // Passthrough (no operators) @@ -508,8 +508,8 @@ describe("sanitizeQuery: regex filters", () => { test("passes through a regex on array keys", () => { expect( - sanitizeQuery("arr[*]://a OR [b,]// tags[k,array][*]://a OR b//"), - ).toBe("arr[*]://a OR [b,]// tags[k,array][*]://a OR b//"); + sanitizeQuery("arr[*]://a OR [b,]// tags[k[*],array]://a OR b//"), + ).toBe("arr[*]://a OR [b,]// tags[k[*],array]://a OR b//"); }); test("passes through a regex right after a closing paren or quote", () => { @@ -531,6 +531,76 @@ describe("sanitizeQuery: regex filters", () => { }); }); +describe("parseSearchNodes: shared Sentry grammar", () => { + test("splits multi-word free text into one node per word", () => { + expect(parseSearchNodes("database timeout level:error")).toEqual([ + { type: "free_text", value: "database", quoted: false }, + { type: "free_text", value: "timeout", quoted: false }, + { type: "text_filter", negated: false, key: "level", value: "error" }, + ]); + }); + + test("keeps quotes on quoted free text and values", () => { + expect(parseSearchNodes('"foo bar" message:"a b"')).toEqual([ + { type: "free_text", value: '"foo bar"', quoted: true }, + { type: "text_filter", negated: false, key: "message", value: '"a b"' }, + ]); + }); + + test("reads typed and aggregate filters as comparisons", () => { + expect( + parseSearchNodes("transaction.duration:>1s count():>=10 age:-24h"), + ).toEqual([ + { + type: "comparison_filter", + negated: false, + key: "transaction.duration", + op: ">", + value: "1s", + }, + { + type: "comparison_filter", + negated: false, + key: "count()", + op: ">=", + value: "10", + }, + { type: "text_filter", negated: false, key: "age", value: "-24h" }, + ]); + }); + + test("returns null for unbalanced parentheses", () => { + expect(parseSearchNodes("(level:error OR level:warning")).toBeNull(); + }); + + test("keeps filters with an empty value or list item opaque", () => { + expect(parseSearchNodes("level: release:[a,,b]")).toEqual([ + { type: "opaque", raw: "level:" }, + { type: "opaque", raw: "release:[a,,b]" }, + ]); + }); +}); + +describe("sanitizeQuery: shared grammar behavior", () => { + test("passes unbalanced parentheses through to the API", () => { + expect(sanitizeQuery("(level:error OR level:warning")).toBe( + "(level:error OR level:warning", + ); + }); + + test("refuses to merge aggregate comparisons", () => { + expect(() => sanitizeQuery("count():>10 OR count():>5")).toThrow( + ValidationError, + ); + }); + + test("strips AND between multi-word free text and filters", () => { + expect(sanitizeQuery("database timeout AND level:error")).toBe( + "database timeout level:error", + ); + }); +}); + describe("normalizeQuery: pre-parse text normalization", () => { describe("mismatched brackets", () => { test("fixes wrong closing delimiter ) → ]", () => { diff --git a/packages/mcp-core/src/tools/support/search-events/utils.test.ts b/packages/mcp-core/src/tools/support/search-events/utils.test.ts index 23ee77616..38e42ba85 100644 --- a/packages/mcp-core/src/tools/support/search-events/utils.test.ts +++ b/packages/mcp-core/src/tools/support/search-events/utils.test.ts @@ -379,7 +379,21 @@ describe("search query helpers", () => { expect(looksLikeSentrySearchSyntax('"Note"message://a b//', "logs")).toBe( true, ); - expect(looksLikeSentrySearchSyntax("(level:error)", "logs")).toBe(false); + // The parser reads filters inside parentheses like any other filter. + expect(looksLikeSentrySearchSyntax("(level:error)", "logs")).toBe(true); + expect(looksLikeSentrySearchSyntax("(level:error OR level:warning)")).toBe( + true, + ); + }); + + it("should read structured filters the way Sentry's grammar does", () => { + expect(looksLikeSentrySearchSyntax("count():>10")).toBe(true); + expect(looksLikeSentrySearchSyntax("p95(span.duration):>200")).toBe(true); + expect(looksLikeSentrySearchSyntax("level:error\nchecking out")).toBe(true); + expect(looksLikeSentrySearchSyntax("meeting 10.30:45 tomorrow")).toBe( + false, + ); + expect(looksLikeSentrySearchSyntax("level:")).toBe(false); }); it("should not treat regex filters as search syntax outside logs", () => { diff --git a/packages/mcp-core/src/tools/support/search-events/utils.ts b/packages/mcp-core/src/tools/support/search-events/utils.ts index 402c70dc8..02e1e7e70 100644 --- a/packages/mcp-core/src/tools/support/search-events/utils.ts +++ b/packages/mcp-core/src/tools/support/search-events/utils.ts @@ -1,4 +1,11 @@ import { isAggregateFieldOrEquation } from "@sentry/toolkit-core/events-fields"; +import { + type FilterToken, + type ParseResultToken, + parseSearch, + TermOperator, + Token, +} from "@sentry/toolkit-core/search-syntax"; import { getActiveSpan } from "@sentry/core"; import { z } from "zod"; import type { @@ -35,7 +42,6 @@ const DEFAULT_MAX_ARRAY_ITEMS = 20; // separates a key://pattern// filter from prose like http://example.com. const REGEX_FILTER_VALUE_SOURCE = String.raw`\/\/(?!\/\/(?:[\t\n )]|$))[^\n]{1,1024}?\/\/(?=[\t\n )]|$)`; const REGEX_FILTER_VALUE_PATTERN = new RegExp(`^${REGEX_FILTER_VALUE_SOURCE}`); -const SEARCH_FILTER_KEY_SOURCE = String.raw`(^|\s)!?(?[A-Za-z_][A-Za-z0-9_.[\],-]*):`; // Sentry would parse `https://host/path// ` as a regex filter too, but no // attribute is named after a URL scheme, so treat those as plain text. const URL_SCHEME_KEY_SOURCE = @@ -48,14 +54,17 @@ const REGEX_FILTER_KEY_BEFORE_PATTERN = new RegExp( `${REGEX_FILTER_KEY_SOURCE}$`, ); const REGEX_FILTER_KEY_SCAN_LIMIT = 256; -const SENTRY_SEARCH_TOKEN_PATTERN = new RegExp( - String.raw`${SEARCH_FILTER_KEY_SOURCE}(?=\S)(?!\/\/)`, - "g", -); +const SEARCH_FILTER_KEY_SOURCE = String.raw`(^|\s)!?(?[A-Za-z_][A-Za-z0-9_.[\],-]*):`; +// Used by the semantic-downgrade comparison below, which still scans the raw +// query text. `looksLikeSentrySearchSyntax` parses with the shared grammar. const SENTRY_SEARCH_TOKEN_WITH_REGEX_PATTERN = new RegExp( String.raw`${REGEX_FILTER_KEY_SOURCE}(?=${REGEX_FILTER_VALUE_SOURCE})|${SEARCH_FILTER_KEY_SOURCE}(?=\S)(?!\/\/)`, "g", ); +// Sentry would parse `https://host/path// ` as a regex filter too; see +// URL_SCHEME_KEY_SOURCE. +const URL_SCHEME_KEY_PATTERN = /^(?:https?|wss?|s?ftp|file|s3|gs)$/i; +const LINE_BREAK_PATTERN = /[\r\n]+/g; const KNOWN_SENTRY_SEARCH_KEYS = new Set([ "browser", "device", @@ -113,50 +122,86 @@ export function isAggregateQuery(fields: readonly string[]): boolean { type SearchSyntaxDataset = EventsDataset | "replays"; -// Sentry only honors key://pattern// as a regex on logs; other datasets match -// the literal text //pattern//. -function searchTokenPattern(dataset?: SearchSyntaxDataset): RegExp { - return dataset === "logs" - ? SENTRY_SEARCH_TOKEN_WITH_REGEX_PATTERN - : SENTRY_SEARCH_TOKEN_PATTERN; +/** Every filter token, including those inside parenthesized groups. */ +function* filterTokens( + tokens: readonly ParseResultToken[], +): Generator { + for (const token of tokens) { + if (token.type === Token.FILTER) { + yield token; + } else if (token.type === Token.LOGIC_GROUP) { + yield* filterTokens(token.inner); + } + } +} + +/** + * Whether a parsed filter is Sentry search syntax rather than prose that + * happens to contain a colon, like `Note: ...`, `10:30`, or a URL. + */ +function isStructuredFilter(query: string, filter: FilterToken): boolean { + const key = query.slice( + filter.key.location.start.offset, + filter.key.location.end.offset, + ); + + if (filter.operator === TermOperator.MATCHES) { + // No attribute is named after a URL scheme, so `https://host/path//` is + // prose even where Sentry would read it as a regex. + return !( + filter.key.type === Token.KEY_SIMPLE && URL_SCHEME_KEY_PATTERN.test(key) + ); + } + + const value = query.slice( + filter.key.location.end.offset + 1, + filter.location.end.offset, + ); + // An empty value is `Word: ...`; a `//` value is a URL, or a regex on a + // dataset that matches it as literal text. + if (!value || value.startsWith("//") || !/^[A-Za-z_]/.test(key)) { + return false; + } + + if (filter.key.type === Token.KEY_AGGREGATE || /^tags\[[^\]]+\]$/.test(key)) { + return true; + } + + if (key !== key.toLowerCase()) { + return false; + } + + return ( + KNOWN_SENTRY_SEARCH_KEYS.has(key) || + /[.[\],]/.test(key) || + /^[a-z_][a-z0-9_-]*$/.test(key) + ); } export function looksLikeSentrySearchSyntax( query?: string, dataset?: SearchSyntaxDataset, ): boolean { - const trimmedQuery = query?.trim(); + // The grammar rejects line breaks outside quotes; they only separate terms. + const trimmedQuery = query?.trim().replace(LINE_BREAK_PATTERN, " "); if (!trimmedQuery) { return false; } - for (const match of trimmedQuery.matchAll(searchTokenPattern(dataset))) { - if (match.groups?.regexKey) { - return true; - } - - const key = match.groups?.key; - if (!key) { - continue; - } - - if (/^tags\[[^\]]+\]$/.test(key)) { - return true; - } - - if (key !== key.toLowerCase()) { - continue; - } - - if (KNOWN_SENTRY_SEARCH_KEYS.has(key) || /[.[\],]/.test(key)) { - return true; - } + // Sentry only honors key://pattern// as a regex on logs; other datasets + // match the literal text //pattern//. + const tokens = parseSearch(trimmedQuery, { + config: { allowRegex: dataset === "logs" }, + }); + if (!tokens) { + return false; + } - if (/^[a-z_][a-z0-9_-]*$/.test(key)) { + for (const filter of filterTokens(tokens)) { + if (isStructuredFilter(trimmedQuery, filter)) { return true; } } - return false; } diff --git a/packages/toolkit-core/README.md b/packages/toolkit-core/README.md index c6b286d8f..5ab41b480 100644 --- a/packages/toolkit-core/README.md +++ b/packages/toolkit-core/README.md @@ -7,13 +7,32 @@ The shared code validates and formats upstream bearer tokens, assembles Sentry API URLs, constructs OAuth device-flow form bodies, classifies RFC 8628 polling responses, advances retry intervals, recognizes Sentry hostnames and credential-safe SaaS HTTPS origins, encodes API path identifiers, parses Events -field expressions the way the Sentry frontend does, resolves Events endpoint -sorts so the sort column is always selected, and deduplicates in-flight region -lookups. Each product retains its own credential storage, -regional cache identity and persistence, host allowlists, routing, polling +field expressions the way the Sentry frontend does, parses search queries with +the Sentry frontend grammar, resolves Events endpoint sorts so the sort column +is always selected, and deduplicates in-flight region lookups. Each product +retains its own credential storage, regional cache identity and persistence, +host allowlists, routing, polling deadline, HTTP transport, response validation, and user-facing errors. The `resource-identity` types describe the identifiers shared by organization, project, team, and issue responses. CLI overlays retain SDK-derived string IDs; MCP schemas accept numeric IDs too and check their parsed outputs against these types. Each product keeps its own response validation. + +## Search syntax + +`search-syntax` parses Sentry search queries with the Sentry frontend's own +grammar. `grammar/search-syntax.pegjs` is an unchanged copy of +[`static/app/components/searchSyntax/grammar.pegjs`](https://github.com/getsentry/sentry/blob/master/static/app/components/searchSyntax/grammar.pegjs), +and `test/fixtures/search-syntax/` is a copy of the `fixtures/search-syntax` +cases the frontend and `event_search.py` both test against. `parseSearch` +returns the frontend's token tree, minus validation: `TokenConverter` never +marks tokens invalid or fills `parsed` values. A product that needs those +subclasses `TokenConverter` and passes it as `converter`. + +The parser is compiled ahead of time into `src/generated/` and committed, +since this package has no build step. After editing a grammar, run +`pnpm --filter @sentry/toolkit-core generate`; the pre-commit hook does this +for staged grammar changes, and `pnpm run check:generated` fails in CI when +the committed parser is out of date. When updating the grammar from upstream, +copy the fixtures too. diff --git a/packages/toolkit-core/grammar/search-syntax.pegjs b/packages/toolkit-core/grammar/search-syntax.pegjs new file mode 100644 index 000000000..2ffab55dd --- /dev/null +++ b/packages/toolkit-core/grammar/search-syntax.pegjs @@ -0,0 +1,541 @@ +{ + const {TokenConverter, TermOperator, FilterType, config} = options; + const tc = new TokenConverter({text, location, config}); + + const opDefault = TermOperator.DEFAULT; +} + +search + = space:spaces terms:term* { + return [space, ...terms.flat()]; + } + +term + = (boolean_operator / paren_group / open_paren / closed_paren / filter / free_text) spaces + +term_no_paren + = (boolean_operator / paren_group / filter / free_text) spaces + +boolean_operator + = (or_operator / and_operator) { + return tc.tokenLogicBoolean(text().toUpperCase()); + } + +paren_group + = open_paren spaces:spaces inner:term_no_paren* closed_paren &{ + return tc.predicateParenGroup(); + } { + return tc.tokenLogicGroup([spaces, ...inner].flat()); + } + +free_text + = free_text_quoted / free_text_unquoted + +free_text_unquoted + = (!filter_start !boolean_operator (free_parens / [^()\n ]+) spaces)+ { + return tc.tokenFreeText(text(), false); + } + +free_text_quoted + = value:quoted_value { + return tc.tokenFreeText(value.value, true); + } + +free_parens + = open_paren free_text? closed_paren + +// All key:value filter types + +filter_start + = negation? ((array_includes_key / text_key / search_key) sep / aggregate_filter_start) + +aggregate_filter_start + = &aggregate_key_start &aggregate_filter + +aggregate_key_start + = [a-zA-Z0-9_.-]+ "(" + +filter + = date_filter + / specific_date_filter + / rel_date_filter + / duration_filter + / size_filter + / boolean_filter + / numeric_in_filter + / numeric_filter + / aggregate_filter + / has_filter + / is_filter + / regex_filter + / array_includes_filter + / text_in_filter + / text_filter + +aggregate_filter + = aggregate_duration_filter + / aggregate_size_filter + / aggregate_numeric_filter + / aggregate_percentage_filter + / aggregate_date_filter + / aggregate_rel_date_filter + +// filter for dates +date_filter + = key:search_key sep op:operator value:iso_8601_date_format &{ + return tc.predicateFilter(FilterType.DATE, key, value, op) + } { + return tc.tokenFilter(FilterType.DATE, key, value, op, false); + } + +// filter for exact dates +specific_date_filter + = key:search_key sep value:iso_8601_date_format &{ + return tc.predicateFilter(FilterType.SPECIFIC_DATE, key) + } { + return tc.tokenFilter(FilterType.SPECIFIC_DATE, key, value, opDefault, false); + } + +// filter for relative dates +rel_date_filter + = key:search_key sep value:rel_date_format &{ + return tc.predicateFilter(FilterType.RELATIVE_DATE, key) + } { + return tc.tokenFilter(FilterType.RELATIVE_DATE, key, value, opDefault, false); + } + +// filter for durations +duration_filter + = negation:negation? key:search_key sep op:operator? value:duration_format &{ + return tc.predicateFilter(FilterType.DURATION, key) + } { + return tc.tokenFilter(FilterType.DURATION, key, value, op, !!negation); + } + +// filter for file size +size_filter + = negation:negation? key:search_key sep op:operator? value:size_format &{ + return tc.predicateFilter(FilterType.SIZE, key) + } { + return tc.tokenFilter(FilterType.SIZE, key, value, op, !!negation); + } + +// boolean comparison filter +boolean_filter + = negation:negation? key:search_key sep value:boolean_value &{ + return tc.predicateFilter(FilterType.BOOLEAN, key) + } { + return tc.tokenFilter(FilterType.BOOLEAN, key, value, opDefault, !!negation); + } + +// numeric in filter +numeric_in_filter + = negation:negation? key:search_key sep value:numeric_in_list &{ + return tc.predicateFilter(FilterType.NUMERIC_IN, key) + } { + return tc.tokenFilter(FilterType.NUMERIC_IN, key, value, opDefault, !!negation); + } + +// numeric comparison filter +numeric_filter + = negation:negation? key:search_key sep op:operator? value:numeric_value &{ + return tc.predicateFilter(FilterType.NUMERIC, key) + } { + return tc.tokenFilter(FilterType.NUMERIC, key, value, op, !!negation); + } + +// aggregate duration filter +aggregate_duration_filter + = negation:negation? key:aggregate_key sep op:operator? value:duration_format &{ + return tc.predicateFilter(FilterType.AGGREGATE_DURATION, key) + } { + return tc.tokenFilter(FilterType.AGGREGATE_DURATION, key, value, op, !!negation); + } + +// aggregate file size filter +aggregate_size_filter + = negation:negation? key:aggregate_key sep op:operator? value:size_format &{ + return tc.predicateFilter(FilterType.AGGREGATE_SIZE, key) + } { + return tc.tokenFilter(FilterType.AGGREGATE_SIZE, key, value, op, !!negation); + } + +// aggregate percentage filter +aggregate_percentage_filter + = negation:negation? key:aggregate_key sep op:operator? value:percentage_format &{ + return tc.predicateFilter(FilterType.AGGREGATE_PERCENTAGE, key) + } { + return tc.tokenFilter(FilterType.AGGREGATE_PERCENTAGE, key, value, op, !!negation); + } + +// aggregate numeric filter +aggregate_numeric_filter + = negation:negation? key:aggregate_key sep op:operator? value:numeric_value &{ + return tc.predicateFilter(FilterType.AGGREGATE_NUMERIC, key) + } { + return tc.tokenFilter(FilterType.AGGREGATE_NUMERIC, key, value, op, !!negation); + } + +// aggregate date filter +aggregate_date_filter + = negation:negation? key:aggregate_key sep op:operator? value:iso_8601_date_format &{ + return tc.predicateFilter(FilterType.AGGREGATE_DATE, key) + } { + return tc.tokenFilter(FilterType.AGGREGATE_DATE, key, value, op, !!negation); + } + +// filter for relative dates +aggregate_rel_date_filter + = negation:negation? key:aggregate_key sep op:operator? value:rel_date_format &{ + return tc.predicateFilter(FilterType.AGGREGATE_RELATIVE_DATE, key) + } { + return tc.tokenFilter(FilterType.AGGREGATE_RELATIVE_DATE, key, value, op, !!negation); + } + +// has filter for not null type checks +has_filter + = negation:negation? &"has:" key:search_key sep value:(search_value/search_key) &{ + return tc.predicateFilter(FilterType.HAS, key) + } { + return tc.tokenFilter(FilterType.HAS, key, value, opDefault, !!negation); + } + +// is filter. Specific to issue search +is_filter + = negation:negation? &"is:" key:search_key sep value:search_value &{ + return tc.predicateFilter(FilterType.IS, key) + } { + return tc.tokenFilter(FilterType.IS, key, value, opDefault, !!negation); + } + +// Array membership filter, eg. `foo[*]:value`. The `[*]` on the key carries the +// operation, so it uses the default operator (`:`) with `!` for negation. +array_includes_filter + = negation:negation? + key:array_includes_key + sep + value:search_value &{ + return tc.predicateFilter(FilterType.ARRAY_INCLUDES, key) + } { + return tc.tokenFilter( + FilterType.ARRAY_INCLUDES, + key, + value, + opDefault, + !!negation, + undefined, + ); + } + +regex_filter + = negation:negation? + key:(array_includes_key / text_key) + sep + value:regex_value { + return tc.tokenRegexFilter(key, value.literal, value.pattern, !!negation); + } + +regex_value + = "//" pattern:regex_pattern "//" &end_value { + return {pattern, literal: tc.tokenValueText(text(), false)}; + } + +regex_pattern + = (!("//" end_value) [^\n])* { + return tc.tokenValueText(text(), false); + } + +// in filter key:[val1, val2] +text_in_filter + = negation:negation? + key:text_key + sep + wildcard_op:wildcard_op? + value:text_in_list &{ + return tc.predicateFilter(FilterType.TEXT_IN, key) + } { + const wildcard = wildcard_op ? wildcard_op.join("") : undefined; + return tc.tokenFilter( + FilterType.TEXT_IN, + key, + value, + opDefault, + !!negation, + wildcard, + ); + } + +// standard key:val filter +// +// The text_filter is a little special since it may not have an operator +// depending on the configuration of the search parser, thus we have a +// predicate for the operator. +text_filter + = negation:negation? + key:text_key + sep + wildcard_op:wildcard_op? + op:(operator &{ return tc.predicateTextOperator(key); })? + value:search_value &{ + return tc.predicateFilter(FilterType.TEXT, key) + } { + const wildcard = wildcard_op ? wildcard_op.join("") : undefined; + return tc.tokenFilter( + FilterType.TEXT, + key, + value, + op ? op[0] : opDefault, + !!negation, + wildcard, + ); + } + +// Filter keys +key + = key:$[a-zA-Z0-9_.-]+ { + return tc.tokenKeySimple(key, false); + } + +escaped_key + = key:$[a-zA-Z0-9_.:-]+ { + return tc.tokenKeySimple(key, false); + } + +quoted_key + = '"' key:escaped_key '"' { + return tc.tokenKeySimple(key.value, true); + } + +explicit_flag_key + = prefix:"flags" open_bracket key:escaped_key closed_bracket { + return tc.tokenKeyExplicitFlag(prefix, key); + } + +explicit_string_flag_key + = prefix:"flags" open_bracket key:escaped_key spaces comma spaces 'string' closed_bracket { + return tc.tokenKeyExplicitStringFlag(prefix, key) + } + +explicit_number_flag_key + = prefix:"flags" open_bracket key:escaped_key spaces comma spaces 'number' closed_bracket { + return tc.tokenKeyExplicitNumberFlag(prefix, key) + } + +explicit_tag_key + = prefix:"tags" open_bracket key:escaped_key closed_bracket { + return tc.tokenKeyExplicitTag(prefix, key); + } + +explicit_string_tag_key + = prefix:"tags" open_bracket key:escaped_key spaces comma spaces 'string' closed_bracket { + return tc.tokenKeyExplicitStringTag(prefix, key) + } + +explicit_number_tag_key + = prefix:"tags" open_bracket key:escaped_key spaces comma spaces 'number' closed_bracket { + return tc.tokenKeyExplicitNumberTag(prefix, key) + } + +explicit_boolean_tag_key + = prefix:"tags" open_bracket key:escaped_key spaces comma spaces 'boolean' closed_bracket { + return tc.tokenKeyExplicitBooleanTag(prefix, key) + } + +explicit_array_tag_key + = prefix:"tags" open_bracket key:escaped_key spaces comma spaces 'array' closed_bracket { + return tc.tokenKeyExplicitArrayTag(prefix, key) + } + +// `[*]` is the required array membership operator ("any element equals value"), +// the array analogue of scalar `:`. A future `[N]` would target one index. +array_access_suffix = open_bracket "*" closed_bracket + +array_includes_tag_key + = prefix:"tags" open_bracket key:escaped_key array_access_suffix spaces comma spaces 'array' closed_bracket { + return tc.tokenKeyArrayIncludes(tc.tokenKeyExplicitArrayTag(prefix, key), "*"); + } + +array_includes_attr_key + = base:key array_access_suffix { + return tc.tokenKeyArrayIncludes(base, "*"); + } + +array_includes_key + = array_includes_tag_key / array_includes_attr_key + +aggregate_key + = name:key open_paren s1:spaces args:function_args? s2:spaces closed_paren { + return tc.tokenKeyAggregate(name, args, s1, s2); + } + +function_args + = arg1:aggregate_param + args:(spaces comma spaces (!comma aggregate_param)?)* { + return tc.tokenKeyAggregateArgs(arg1, args); + } + +aggregate_param + = explicit_tag_aggregate_param / quoted_aggregate_param / raw_aggregate_param + +raw_aggregate_param + = param:$[^()\t\n, \"]+ { + return tc.tokenKeyAggregateParam(param, false); + } + +quoted_aggregate_param + = '"' param:$('\\"' / [^\t\n\"])* '"' { + return tc.tokenKeyAggregateParam(`"${param}"`, true); + } + +explicit_tag_aggregate_param + = key:(explicit_tag_key / explicit_string_tag_key / explicit_number_tag_key / explicit_boolean_tag_key) { + return tc.tokenKeyAggregateParam(key.text, false); + } + +search_key + = explicit_number_flag_key / explicit_number_tag_key / explicit_boolean_tag_key / key / quoted_key + +text_key + = explicit_flag_key / explicit_string_flag_key / explicit_tag_key / explicit_string_tag_key / explicit_array_tag_key / search_key + +// Filter values + +value + = value:$[^()\t\n ]* { + return tc.tokenValueText(value, false); + } + +quoted_value + = '"' value:$('\\"' / [^"])* '"' { + return tc.tokenValueText(value, true); + } + +in_value + = (&in_value_termination in_value_char)+ { + return tc.tokenValueText(text(), false); + } + +text_in_value + = quoted_value / in_value + +search_value + = quoted_value / value + +numeric_value + = value:("-"? numeric) unit:(number_unit)? &(end_value / comma / closed_bracket) { + return tc.tokenValueNumber(value.join(''), unit); + } + +boolean_value + = value:("true"i / "1" / "false"i / "0") &end_value { + return tc.tokenValueBoolean(value); + } + +text_in_list + = open_bracket + item1:text_in_value + items:(spaces comma spaces (!comma text_in_value)?)* + closed_bracket + &end_value { + return tc.tokenValueTextList(item1, items); + } + +numeric_in_list + = open_bracket + item1:numeric_value + items:(spaces comma spaces (!comma numeric_value)?)* + closed_bracket + &end_value { + return tc.tokenValueNumberList(item1, items); + } + +// NOTE: These wildcard operators are internal implementation details and +// should not be included in product docs. Users should use `*` instead. +wildcard_op + = wildcard_unicode + (contains / starts_with / ends_with ) + wildcard_unicode + +// See: https://stackoverflow.com/a/39617181/790169 +in_value_termination + = in_value_char (!in_value_end in_value_char)* in_value_end + +in_value_char + = [^(), ] + +in_value_end + = closed_bracket / (spaces comma) + +// Format values + +// XXX: Since pegjs does not support regex there is no easy way to repeat +// groups n times. So we have some dumb tokens here to handle that. We don't do +// this in the backend grammar since we just use regex there. +num2 = [0-9] [0-9] +num4 = [0-9] [0-9] [0-9] [0-9] + +date_format = num4 "-" num2 "-" num2 +time_format = "T" num2 ":" num2 ":" num2 ("." ms_format)? +ms_format = [0-9] [0-9]? [0-9]? [0-9]? [0-9]? [0-9]? +tz_format = [+-] num2 ":" num2 + +iso_8601_date_format + = date:date_format time:time_format? tz:("Z" / tz_format)? &end_value { + return tc.tokenValueIso8601Date(text(), date, time, tz); + } + +rel_date_format + = sign:[+-] value:$[0-9]+ unit:[wdhm] &end_value { + return tc.tokenValueRelativeDate(value, sign, unit); + } + +duration_format + = value:numeric + unit:(duration_unit) + &end_value { + return tc.tokenValueDuration(value, unit); + } + +size_format + = value:numeric + unit:(size_unit) + &end_value { + return tc.tokenValueSize(value, unit); + } + +percentage_format + = value:numeric "%" { + return tc.tokenValuePercentage(value); + } + +// Units for special values +number_unit = "k"i/"m"i/"b"i + +duration_unit = "ms"/"s"/"min"/"m"/"hr"/"h"/"day"/"d"/"wk"/"w" +size_unit = bit_unit / byte_unit + +bit_unit = "bit"i / "kib"i / "mib"i / "gib"i / "tib"i / "pib"i / "eib"i / "zib"i / "yib"i +byte_unit = "bytes"i / "nb"i / "kb"i / "mb"i / "gb"i / "tb"i / "pb"i / "eb"i / "zb"i / "yb"i + +// NOTE: the order in which these operators are listed matters because for +// example, if < comes before <= it will match that even if the operator is <= +operator = ">=" / "<=" / ">" / "<" / "=" / "!=" +or_operator = "OR"i &end_value +and_operator = "AND"i &end_value +numeric = [0-9]+ ("." [0-9]*)? { return text(); } +open_paren = "(" { return tc.tokenLParen(text()); } +closed_paren = ")" { return tc.tokenRParen(text()); } +open_bracket = "[" +closed_bracket = "]" +sep = ":" +negation = "!" +// NOTE: These wildcard operators are internal implementation details and +// should not be included in product docs. Users should use `*` instead. +wildcard_unicode = [\uF00D] +contains = "Contains" +starts_with = "StartsWith" +ends_with = "EndsWith" +comma = "," +spaces = " "* { return tc.tokenSpaces(text()) } + +end_value = [\t\n )] / !. diff --git a/packages/toolkit-core/package.json b/packages/toolkit-core/package.json index 1b98a438e..3f4ff17c7 100644 --- a/packages/toolkit-core/package.json +++ b/packages/toolkit-core/package.json @@ -43,6 +43,10 @@ "types": "./src/resource-identity.ts", "default": "./src/resource-identity.ts" }, + "./search-syntax": { + "types": "./src/search-syntax.ts", + "default": "./src/search-syntax.ts" + }, "./sentry-host": { "types": "./src/sentry-host.ts", "default": "./src/sentry-host.ts" @@ -53,11 +57,13 @@ } }, "scripts": { + "generate": "tsx script/generate-parsers.ts", "test": "vitest run", "test:ci": "vitest run --reporter=default --reporter=junit --outputFile=tests.junit.xml", "tsc": "tsc --noEmit" }, "devDependencies": { + "peggy": "^5.1.0", "typescript": "catalog:", "vitest": "catalog:" } diff --git a/packages/toolkit-core/script/generate-parsers.ts b/packages/toolkit-core/script/generate-parsers.ts new file mode 100644 index 000000000..b789918f1 --- /dev/null +++ b/packages/toolkit-core/script/generate-parsers.ts @@ -0,0 +1,88 @@ +#!/usr/bin/env tsx +/** + * Compile the shared PEG grammars into committed ES module parsers. + * + * toolkit-core ships TypeScript source with no build step, so the CLI, the + * stdio MCP server, and the Cloudflare worker all import the generated + * parsers straight from `src/generated/`. `pnpm run check:generated` in the + * workspace root regenerates them and fails when the committed output is out + * of date. + * + * `grammar/search-syntax.pegjs` is a byte-for-byte copy of the Sentry + * frontend grammar at + * https://github.com/getsentry/sentry/blob/master/static/app/components/searchSyntax/grammar.pegjs + * so the two can converge on one source. + * + * Usage: + * pnpm --filter @sentry/toolkit-core generate + */ + +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import peggy from "peggy"; + +const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const HEADER = + "// Auto-generated by packages/toolkit-core/script/generate-parsers.ts — DO NOT EDIT\n"; + +type GrammarSpec = { + grammar: string; + output: string; + allowedStartRules: string[]; + declarations: string; +}; + +const GRAMMARS: GrammarSpec[] = [ + { + grammar: "grammar/search-syntax.pegjs", + output: "src/generated/search-syntax-grammar", + allowedStartRules: ["search"], + declarations: `${HEADER} +/** + * Options the search grammar reads from \`options\`. The grammar builds every + * token through \`TokenConverter\`, so callers decide the token shapes and the + * key-type predicates. See \`../search-syntax.ts\` for the default converter. + */ +export type SearchGrammarOptions = { + /** Constructed with \`{ text, location, config }\` once per parse. */ + TokenConverter: new (opts: never) => unknown; + TermOperator: { DEFAULT: string }; + FilterType: Record; + config: unknown; + startRule?: "search"; +}; + +export declare class SyntaxError extends Error { + expected: unknown; + found: string | null; + location: unknown; +} + +/** Parse a search query. Throws {@link SyntaxError} when it does not match. */ +export declare function parse( + input: string, + options: SearchGrammarOptions, +): unknown[]; +`, + }, +]; + +for (const spec of GRAMMARS) { + const grammarSource = await readFile( + resolve(PACKAGE_ROOT, spec.grammar), + "utf-8", + ); + const parserSource = peggy.generate(grammarSource, { + output: "source", + format: "es", + allowedStartRules: spec.allowedStartRules, + grammarSource: spec.grammar, + }); + + const outputBase = resolve(PACKAGE_ROOT, spec.output); + await mkdir(dirname(outputBase), { recursive: true }); + await writeFile(`${outputBase}.js`, `${HEADER}\n${parserSource}\n`); + await writeFile(`${outputBase}.d.ts`, spec.declarations); + console.log(`Generated ${spec.output}.js and ${spec.output}.d.ts`); +} diff --git a/packages/toolkit-core/src/generated/search-syntax-grammar.d.ts b/packages/toolkit-core/src/generated/search-syntax-grammar.d.ts new file mode 100644 index 000000000..9e69d214c --- /dev/null +++ b/packages/toolkit-core/src/generated/search-syntax-grammar.d.ts @@ -0,0 +1,27 @@ +// Auto-generated by packages/toolkit-core/script/generate-parsers.ts — DO NOT EDIT + +/** + * Options the search grammar reads from `options`. The grammar builds every + * token through `TokenConverter`, so callers decide the token shapes and the + * key-type predicates. See `../search-syntax.ts` for the default converter. + */ +export type SearchGrammarOptions = { + /** Constructed with `{ text, location, config }` once per parse. */ + TokenConverter: new (opts: never) => unknown; + TermOperator: { DEFAULT: string }; + FilterType: Record; + config: unknown; + startRule?: "search"; +}; + +export declare class SyntaxError extends Error { + expected: unknown; + found: string | null; + location: unknown; +} + +/** Parse a search query. Throws {@link SyntaxError} when it does not match. */ +export declare function parse( + input: string, + options: SearchGrammarOptions, +): unknown[]; diff --git a/packages/toolkit-core/src/generated/search-syntax-grammar.js b/packages/toolkit-core/src/generated/search-syntax-grammar.js new file mode 100644 index 000000000..7c418005a --- /dev/null +++ b/packages/toolkit-core/src/generated/search-syntax-grammar.js @@ -0,0 +1,5522 @@ +// Auto-generated by packages/toolkit-core/script/generate-parsers.ts — DO NOT EDIT + +// @generated by Peggy 5.1.0. +// +// https://peggyjs.org/ + + +class peg$SyntaxError extends SyntaxError { + constructor(message, expected, found, location) { + super(message); + this.expected = expected; + this.found = found; + this.location = location; + this.name = "SyntaxError"; + } + + format(sources) { + let str = "Error: " + this.message; + if (this.location) { + let src = null; + const st = sources.find(s => s.source === this.location.source); + if (st) { + src = st.text.split(/\r\n|\n|\r/g); + } + const s = this.location.start; + const offset_s = (this.location.source && (typeof this.location.source.offset === "function")) + ? this.location.source.offset(s) + : s; + const loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column; + if (src) { + const e = this.location.end; + const filler = "".padEnd(offset_s.line.toString().length, " "); + const line = src[s.line - 1]; + const last = s.line === e.line ? e.column : line.length + 1; + const hatLen = (last - s.column) || 1; + str += "\n --> " + loc + "\n" + + filler + " |\n" + + offset_s.line + " | " + line + "\n" + + filler + " | " + "".padEnd(s.column - 1, " ") + + "".padEnd(hatLen, "^"); + } else { + str += "\n at " + loc; + } + } + return str; + } + + static buildMessage(expected, found) { + function hex(ch) { + return ch.codePointAt(0).toString(16).toUpperCase(); + } + + const nonPrintable = Object.prototype.hasOwnProperty.call(RegExp.prototype, "unicode") + ? new RegExp("[\\p{C}\\p{Mn}\\p{Mc}]", "gu") + : null; + function unicodeEscape(s) { + if (nonPrintable) { + return s.replace(nonPrintable, ch => "\\u{" + hex(ch) + "}"); + } + return s; + } + + function literalEscape(s) { + return unicodeEscape(s + .replace(/\\/g, "\\\\") + .replace(/"/g, "\\\"") + .replace(/\0/g, "\\0") + .replace(/\t/g, "\\t") + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r") + .replace(/[\x00-\x0F]/g, ch => "\\x0" + hex(ch)) + .replace(/[\x10-\x1F\x7F-\x9F]/g, ch => "\\x" + hex(ch))); + } + + function classEscape(s) { + return unicodeEscape(s + .replace(/\\/g, "\\\\") + .replace(/\]/g, "\\]") + .replace(/\^/g, "\\^") + .replace(/-/g, "\\-") + .replace(/\0/g, "\\0") + .replace(/\t/g, "\\t") + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r") + .replace(/[\x00-\x0F]/g, ch => "\\x0" + hex(ch)) + .replace(/[\x10-\x1F\x7F-\x9F]/g, ch => "\\x" + hex(ch))); + } + + const DESCRIBE_EXPECTATION_FNS = { + literal(expectation) { + return "\"" + literalEscape(expectation.text) + "\""; + }, + + class(expectation) { + const escapedParts = expectation.parts.map( + part => (Array.isArray(part) + ? classEscape(part[0]) + "-" + classEscape(part[1]) + : classEscape(part)) + ); + + return "[" + (expectation.inverted ? "^" : "") + escapedParts.join("") + "]" + (expectation.unicode ? "u" : ""); + }, + + any() { + return "any character"; + }, + + end() { + return "end of input"; + }, + + other(expectation) { + return expectation.description; + }, + }; + + function describeExpectation(expectation) { + return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation); + } + + function describeExpected(expected) { + const descriptions = expected.map(describeExpectation); + descriptions.sort(); + + if (descriptions.length > 0) { + let j = 1; + for (let i = 1; i < descriptions.length; i++) { + if (descriptions[i - 1] !== descriptions[i]) { + descriptions[j] = descriptions[i]; + j++; + } + } + descriptions.length = j; + } + + switch (descriptions.length) { + case 1: + return descriptions[0]; + + case 2: + return descriptions[0] + " or " + descriptions[1]; + + default: + return descriptions.slice(0, -1).join(", ") + + ", or " + + descriptions[descriptions.length - 1]; + } + } + + function describeFound(found) { + return found ? "\"" + literalEscape(found) + "\"" : "end of input"; + } + + return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found."; + } +} + +function peg$parse(input, options) { + options = options !== undefined ? options : {}; + + const peg$FAILED = {}; + const peg$source = options.grammarSource; + + const peg$startRuleFunctions = { + search: peg$parsesearch, + }; + let peg$startRuleFunction = peg$parsesearch; + + const peg$c0 = "("; + const peg$c1 = "has:"; + const peg$c2 = "is:"; + const peg$c3 = "//"; + const peg$c4 = "\""; + const peg$c5 = "flags"; + const peg$c6 = "string"; + const peg$c7 = "number"; + const peg$c8 = "tags"; + const peg$c9 = "boolean"; + const peg$c10 = "array"; + const peg$c11 = "*"; + const peg$c12 = "\\\""; + const peg$c13 = "-"; + const peg$c14 = "true"; + const peg$c15 = "1"; + const peg$c16 = "false"; + const peg$c17 = "0"; + const peg$c18 = "T"; + const peg$c19 = ":"; + const peg$c20 = "."; + const peg$c21 = "Z"; + const peg$c22 = "%"; + const peg$c23 = "ms"; + const peg$c24 = "s"; + const peg$c25 = "min"; + const peg$c26 = "m"; + const peg$c27 = "hr"; + const peg$c28 = "h"; + const peg$c29 = "day"; + const peg$c30 = "d"; + const peg$c31 = "wk"; + const peg$c32 = "w"; + const peg$c33 = "bit"; + const peg$c34 = "kib"; + const peg$c35 = "mib"; + const peg$c36 = "gib"; + const peg$c37 = "tib"; + const peg$c38 = "pib"; + const peg$c39 = "eib"; + const peg$c40 = "zib"; + const peg$c41 = "yib"; + const peg$c42 = "bytes"; + const peg$c43 = "nb"; + const peg$c44 = "kb"; + const peg$c45 = "mb"; + const peg$c46 = "gb"; + const peg$c47 = "tb"; + const peg$c48 = "pb"; + const peg$c49 = "eb"; + const peg$c50 = "zb"; + const peg$c51 = "yb"; + const peg$c52 = ">="; + const peg$c53 = "<="; + const peg$c54 = "!="; + const peg$c55 = "or"; + const peg$c56 = "and"; + const peg$c57 = ")"; + const peg$c58 = "["; + const peg$c59 = "]"; + const peg$c60 = "!"; + const peg$c61 = "Contains"; + const peg$c62 = "StartsWith"; + const peg$c63 = "EndsWith"; + const peg$c64 = ","; + const peg$c65 = " "; + + const peg$r0 = /^[^()\n ]/; + const peg$r1 = /^[a-zA-Z0-9_.\-]/; + const peg$r2 = /^[^\n]/; + const peg$r3 = /^[a-zA-Z0-9_.:\-]/; + const peg$r4 = /^[^()\t\n, "]/; + const peg$r5 = /^[^\t\n"]/; + const peg$r6 = /^[^()\t\n ]/; + const peg$r7 = /^[^"]/; + const peg$r8 = /^[,\]]/; + const peg$r9 = /^[^(), ]/; + const peg$r10 = /^[0-9]/; + const peg$r11 = /^[+\-]/; + const peg$r12 = /^[wdhm]/; + const peg$r13 = /^[bkm]/i; + const peg$r14 = /^[<->]/; + const peg$r15 = /^[\uF00D]/; + const peg$r16 = /^[\t\n )]/; + + const peg$e0 = peg$classExpectation(["(", ")", "\n", " "], true, false, false); + const peg$e1 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"], "_", ".", "-"], false, false, false); + const peg$e2 = peg$literalExpectation("(", false); + const peg$e3 = peg$literalExpectation("has:", false); + const peg$e4 = peg$literalExpectation("is:", false); + const peg$e5 = peg$literalExpectation("//", false); + const peg$e6 = peg$classExpectation(["\n"], true, false, false); + const peg$e7 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"], "_", ".", ":", "-"], false, false, false); + const peg$e8 = peg$literalExpectation("\"", false); + const peg$e9 = peg$literalExpectation("flags", false); + const peg$e10 = peg$literalExpectation("string", false); + const peg$e11 = peg$literalExpectation("number", false); + const peg$e12 = peg$literalExpectation("tags", false); + const peg$e13 = peg$literalExpectation("boolean", false); + const peg$e14 = peg$literalExpectation("array", false); + const peg$e15 = peg$literalExpectation("*", false); + const peg$e16 = peg$classExpectation(["(", ")", "\t", "\n", ",", " ", "\""], true, false, false); + const peg$e17 = peg$literalExpectation("\\\"", false); + const peg$e18 = peg$classExpectation(["\t", "\n", "\""], true, false, false); + const peg$e19 = peg$classExpectation(["(", ")", "\t", "\n", " "], true, false, false); + const peg$e20 = peg$classExpectation(["\""], true, false, false); + const peg$e21 = peg$literalExpectation("-", false); + const peg$e22 = peg$classExpectation([",", "]"], false, false, false); + const peg$e23 = peg$literalExpectation("true", true); + const peg$e24 = peg$literalExpectation("1", false); + const peg$e25 = peg$literalExpectation("false", true); + const peg$e26 = peg$literalExpectation("0", false); + const peg$e27 = peg$classExpectation(["(", ")", ",", " "], true, false, false); + const peg$e28 = peg$classExpectation([["0", "9"]], false, false, false); + const peg$e29 = peg$literalExpectation("T", false); + const peg$e30 = peg$literalExpectation(":", false); + const peg$e31 = peg$literalExpectation(".", false); + const peg$e32 = peg$classExpectation(["+", "-"], false, false, false); + const peg$e33 = peg$literalExpectation("Z", false); + const peg$e34 = peg$classExpectation(["w", "d", "h", "m"], false, false, false); + const peg$e35 = peg$literalExpectation("%", false); + const peg$e36 = peg$classExpectation(["b", "k", "m"], false, true, false); + const peg$e37 = peg$literalExpectation("ms", false); + const peg$e38 = peg$literalExpectation("s", false); + const peg$e39 = peg$literalExpectation("min", false); + const peg$e40 = peg$literalExpectation("m", false); + const peg$e41 = peg$literalExpectation("hr", false); + const peg$e42 = peg$literalExpectation("h", false); + const peg$e43 = peg$literalExpectation("day", false); + const peg$e44 = peg$literalExpectation("d", false); + const peg$e45 = peg$literalExpectation("wk", false); + const peg$e46 = peg$literalExpectation("w", false); + const peg$e47 = peg$literalExpectation("bit", true); + const peg$e48 = peg$literalExpectation("kib", true); + const peg$e49 = peg$literalExpectation("mib", true); + const peg$e50 = peg$literalExpectation("gib", true); + const peg$e51 = peg$literalExpectation("tib", true); + const peg$e52 = peg$literalExpectation("pib", true); + const peg$e53 = peg$literalExpectation("eib", true); + const peg$e54 = peg$literalExpectation("zib", true); + const peg$e55 = peg$literalExpectation("yib", true); + const peg$e56 = peg$literalExpectation("bytes", true); + const peg$e57 = peg$literalExpectation("nb", true); + const peg$e58 = peg$literalExpectation("kb", true); + const peg$e59 = peg$literalExpectation("mb", true); + const peg$e60 = peg$literalExpectation("gb", true); + const peg$e61 = peg$literalExpectation("tb", true); + const peg$e62 = peg$literalExpectation("pb", true); + const peg$e63 = peg$literalExpectation("eb", true); + const peg$e64 = peg$literalExpectation("zb", true); + const peg$e65 = peg$literalExpectation("yb", true); + const peg$e66 = peg$literalExpectation(">=", false); + const peg$e67 = peg$literalExpectation("<=", false); + const peg$e68 = peg$classExpectation([["<", ">"]], false, false, false); + const peg$e69 = peg$literalExpectation("!=", false); + const peg$e70 = peg$literalExpectation("OR", true); + const peg$e71 = peg$literalExpectation("AND", true); + const peg$e72 = peg$literalExpectation(")", false); + const peg$e73 = peg$literalExpectation("[", false); + const peg$e74 = peg$literalExpectation("]", false); + const peg$e75 = peg$literalExpectation("!", false); + const peg$e76 = peg$classExpectation(["\uF00D"], false, false, false); + const peg$e77 = peg$literalExpectation("Contains", false); + const peg$e78 = peg$literalExpectation("StartsWith", false); + const peg$e79 = peg$literalExpectation("EndsWith", false); + const peg$e80 = peg$literalExpectation(",", false); + const peg$e81 = peg$literalExpectation(" ", false); + const peg$e82 = peg$classExpectation(["\t", "\n", " ", ")"], false, false, false); + const peg$e83 = peg$anyExpectation(); + + function peg$f0(space, terms) { + return [space, ...terms.flat()]; + } + function peg$f1() { + return tc.tokenLogicBoolean(text().toUpperCase()); + } + function peg$f2(spaces, inner) { + return tc.predicateParenGroup(); + } + function peg$f3(spaces, inner) { + return tc.tokenLogicGroup([spaces, ...inner].flat()); + } + function peg$f4() { + return tc.tokenFreeText(text(), false); + } + function peg$f5(value) { + return tc.tokenFreeText(value.value, true); + } + function peg$f6(key, op, value) { + return tc.predicateFilter(FilterType.DATE, key, value, op) + } + function peg$f7(key, op, value) { + return tc.tokenFilter(FilterType.DATE, key, value, op, false); + } + function peg$f8(key, value) { + return tc.predicateFilter(FilterType.SPECIFIC_DATE, key) + } + function peg$f9(key, value) { + return tc.tokenFilter(FilterType.SPECIFIC_DATE, key, value, opDefault, false); + } + function peg$f10(key, value) { + return tc.predicateFilter(FilterType.RELATIVE_DATE, key) + } + function peg$f11(key, value) { + return tc.tokenFilter(FilterType.RELATIVE_DATE, key, value, opDefault, false); + } + function peg$f12(negation, key, op, value) { + return tc.predicateFilter(FilterType.DURATION, key) + } + function peg$f13(negation, key, op, value) { + return tc.tokenFilter(FilterType.DURATION, key, value, op, !!negation); + } + function peg$f14(negation, key, op, value) { + return tc.predicateFilter(FilterType.SIZE, key) + } + function peg$f15(negation, key, op, value) { + return tc.tokenFilter(FilterType.SIZE, key, value, op, !!negation); + } + function peg$f16(negation, key, value) { + return tc.predicateFilter(FilterType.BOOLEAN, key) + } + function peg$f17(negation, key, value) { + return tc.tokenFilter(FilterType.BOOLEAN, key, value, opDefault, !!negation); + } + function peg$f18(negation, key, value) { + return tc.predicateFilter(FilterType.NUMERIC_IN, key) + } + function peg$f19(negation, key, value) { + return tc.tokenFilter(FilterType.NUMERIC_IN, key, value, opDefault, !!negation); + } + function peg$f20(negation, key, op, value) { + return tc.predicateFilter(FilterType.NUMERIC, key) + } + function peg$f21(negation, key, op, value) { + return tc.tokenFilter(FilterType.NUMERIC, key, value, op, !!negation); + } + function peg$f22(negation, key, op, value) { + return tc.predicateFilter(FilterType.AGGREGATE_DURATION, key) + } + function peg$f23(negation, key, op, value) { + return tc.tokenFilter(FilterType.AGGREGATE_DURATION, key, value, op, !!negation); + } + function peg$f24(negation, key, op, value) { + return tc.predicateFilter(FilterType.AGGREGATE_SIZE, key) + } + function peg$f25(negation, key, op, value) { + return tc.tokenFilter(FilterType.AGGREGATE_SIZE, key, value, op, !!negation); + } + function peg$f26(negation, key, op, value) { + return tc.predicateFilter(FilterType.AGGREGATE_PERCENTAGE, key) + } + function peg$f27(negation, key, op, value) { + return tc.tokenFilter(FilterType.AGGREGATE_PERCENTAGE, key, value, op, !!negation); + } + function peg$f28(negation, key, op, value) { + return tc.predicateFilter(FilterType.AGGREGATE_NUMERIC, key) + } + function peg$f29(negation, key, op, value) { + return tc.tokenFilter(FilterType.AGGREGATE_NUMERIC, key, value, op, !!negation); + } + function peg$f30(negation, key, op, value) { + return tc.predicateFilter(FilterType.AGGREGATE_DATE, key) + } + function peg$f31(negation, key, op, value) { + return tc.tokenFilter(FilterType.AGGREGATE_DATE, key, value, op, !!negation); + } + function peg$f32(negation, key, op, value) { + return tc.predicateFilter(FilterType.AGGREGATE_RELATIVE_DATE, key) + } + function peg$f33(negation, key, op, value) { + return tc.tokenFilter(FilterType.AGGREGATE_RELATIVE_DATE, key, value, op, !!negation); + } + function peg$f34(negation, key, value) { + return tc.predicateFilter(FilterType.HAS, key) + } + function peg$f35(negation, key, value) { + return tc.tokenFilter(FilterType.HAS, key, value, opDefault, !!negation); + } + function peg$f36(negation, key, value) { + return tc.predicateFilter(FilterType.IS, key) + } + function peg$f37(negation, key, value) { + return tc.tokenFilter(FilterType.IS, key, value, opDefault, !!negation); + } + function peg$f38(negation, key, value) { + return tc.predicateFilter(FilterType.ARRAY_INCLUDES, key) + } + function peg$f39(negation, key, value) { + return tc.tokenFilter( + FilterType.ARRAY_INCLUDES, + key, + value, + opDefault, + !!negation, + undefined, + ); + } + function peg$f40(negation, key, value) { + return tc.tokenRegexFilter(key, value.literal, value.pattern, !!negation); + } + function peg$f41(pattern) { + return {pattern, literal: tc.tokenValueText(text(), false)}; + } + function peg$f42() { + return tc.tokenValueText(text(), false); + } + function peg$f43(negation, key, wildcard_op, value) { + return tc.predicateFilter(FilterType.TEXT_IN, key) + } + function peg$f44(negation, key, wildcard_op, value) { + const wildcard = wildcard_op ? wildcard_op.join("") : undefined; + return tc.tokenFilter( + FilterType.TEXT_IN, + key, + value, + opDefault, + !!negation, + wildcard, + ); + } + function peg$f45(negation, key, wildcard_op) { return tc.predicateTextOperator(key); } + function peg$f46(negation, key, wildcard_op, op, value) { + return tc.predicateFilter(FilterType.TEXT, key) + } + function peg$f47(negation, key, wildcard_op, op, value) { + const wildcard = wildcard_op ? wildcard_op.join("") : undefined; + return tc.tokenFilter( + FilterType.TEXT, + key, + value, + op ? op[0] : opDefault, + !!negation, + wildcard, + ); + } + function peg$f48(key) { + return tc.tokenKeySimple(key, false); + } + function peg$f49(key) { + return tc.tokenKeySimple(key, false); + } + function peg$f50(key) { + return tc.tokenKeySimple(key.value, true); + } + function peg$f51(prefix, key) { + return tc.tokenKeyExplicitFlag(prefix, key); + } + function peg$f52(prefix, key) { + return tc.tokenKeyExplicitStringFlag(prefix, key) + } + function peg$f53(prefix, key) { + return tc.tokenKeyExplicitNumberFlag(prefix, key) + } + function peg$f54(prefix, key) { + return tc.tokenKeyExplicitTag(prefix, key); + } + function peg$f55(prefix, key) { + return tc.tokenKeyExplicitStringTag(prefix, key) + } + function peg$f56(prefix, key) { + return tc.tokenKeyExplicitNumberTag(prefix, key) + } + function peg$f57(prefix, key) { + return tc.tokenKeyExplicitBooleanTag(prefix, key) + } + function peg$f58(prefix, key) { + return tc.tokenKeyExplicitArrayTag(prefix, key) + } + function peg$f59(prefix, key) { + return tc.tokenKeyArrayIncludes(tc.tokenKeyExplicitArrayTag(prefix, key), "*"); + } + function peg$f60(base) { + return tc.tokenKeyArrayIncludes(base, "*"); + } + function peg$f61(name, s1, args, s2) { + return tc.tokenKeyAggregate(name, args, s1, s2); + } + function peg$f62(arg1, args) { + return tc.tokenKeyAggregateArgs(arg1, args); + } + function peg$f63(param) { + return tc.tokenKeyAggregateParam(param, false); + } + function peg$f64(param) { + return tc.tokenKeyAggregateParam(`"${param}"`, true); + } + function peg$f65(key) { + return tc.tokenKeyAggregateParam(key.text, false); + } + function peg$f66(value) { + return tc.tokenValueText(value, false); + } + function peg$f67(value) { + return tc.tokenValueText(value, true); + } + function peg$f68() { + return tc.tokenValueText(text(), false); + } + function peg$f69(value, unit) { + return tc.tokenValueNumber(value.join(''), unit); + } + function peg$f70(value) { + return tc.tokenValueBoolean(value); + } + function peg$f71(item1, items) { + return tc.tokenValueTextList(item1, items); + } + function peg$f72(item1, items) { + return tc.tokenValueNumberList(item1, items); + } + function peg$f73(date, time, tz) { + return tc.tokenValueIso8601Date(text(), date, time, tz); + } + function peg$f74(sign, value, unit) { + return tc.tokenValueRelativeDate(value, sign, unit); + } + function peg$f75(value, unit) { + return tc.tokenValueDuration(value, unit); + } + function peg$f76(value, unit) { + return tc.tokenValueSize(value, unit); + } + function peg$f77(value) { + return tc.tokenValuePercentage(value); + } + function peg$f78() { return text(); } + function peg$f79() { return tc.tokenLParen(text()); } + function peg$f80() { return tc.tokenRParen(text()); } + function peg$f81() { return tc.tokenSpaces(text()) } + let peg$currPos = options.peg$currPos | 0; + let peg$savedPos = peg$currPos; + const peg$posDetailsCache = [{ line: 1, column: 1 }]; + let peg$maxFailPos = peg$currPos; + let peg$maxFailExpected = options.peg$maxFailExpected || []; + let peg$silentFails = options.peg$silentFails | 0; + + let peg$result; + + if (options.startRule) { + if (!(options.startRule in peg$startRuleFunctions)) { + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + + function text() { + return input.substring(peg$savedPos, peg$currPos); + } + + function offset() { + return peg$savedPos; + } + + function range() { + return { + source: peg$source, + start: peg$savedPos, + end: peg$currPos, + }; + } + + function location() { + return peg$computeLocation(peg$savedPos, peg$currPos); + } + + function expected(description, location) { + location = location !== undefined + ? location + : peg$computeLocation(peg$savedPos, peg$currPos); + + throw peg$buildStructuredError( + [peg$otherExpectation(description)], + input.substring(peg$savedPos, peg$currPos), + location + ); + } + + function error(message, location) { + location = location !== undefined + ? location + : peg$computeLocation(peg$savedPos, peg$currPos); + + throw peg$buildSimpleError(message, location); + } + + function peg$getUnicode(pos = peg$currPos) { + const cp = input.codePointAt(pos); + if (cp === undefined) { + return ""; + } + return String.fromCodePoint(cp); + } + + function peg$literalExpectation(text, ignoreCase) { + return { type: "literal", text, ignoreCase }; + } + + function peg$classExpectation(parts, inverted, ignoreCase, unicode) { + return { type: "class", parts, inverted, ignoreCase, unicode }; + } + + function peg$anyExpectation() { + return { type: "any" }; + } + + function peg$endExpectation() { + return { type: "end" }; + } + + function peg$otherExpectation(description) { + return { type: "other", description }; + } + + function peg$computePosDetails(pos) { + let details = peg$posDetailsCache[pos]; + let p; + + if (details) { + return details; + } else { + if (pos >= peg$posDetailsCache.length) { + p = peg$posDetailsCache.length - 1; + } else { + p = pos; + while (!peg$posDetailsCache[--p]) {} + } + + details = peg$posDetailsCache[p]; + details = { + line: details.line, + column: details.column, + }; + + while (p < pos) { + if (input.charCodeAt(p) === 10) { + details.line++; + details.column = 1; + } else { + details.column++; + } + + p++; + } + + peg$posDetailsCache[pos] = details; + + return details; + } + } + + function peg$computeLocation(startPos, endPos, offset) { + const startPosDetails = peg$computePosDetails(startPos); + const endPosDetails = peg$computePosDetails(endPos); + + const res = { + source: peg$source, + start: { + offset: startPos, + line: startPosDetails.line, + column: startPosDetails.column, + }, + end: { + offset: endPos, + line: endPosDetails.line, + column: endPosDetails.column, + }, + }; + if (offset && peg$source && (typeof peg$source.offset === "function")) { + res.start = peg$source.offset(res.start); + res.end = peg$source.offset(res.end); + } + return res; + } + + function peg$fail(expected) { + if (peg$currPos < peg$maxFailPos) { return; } + + if (peg$currPos > peg$maxFailPos) { + peg$maxFailPos = peg$currPos; + peg$maxFailExpected = []; + } + + peg$maxFailExpected.push(expected); + } + + function peg$buildSimpleError(message, location) { + return new peg$SyntaxError(message, null, null, location); + } + + function peg$buildStructuredError(expected, found, location) { + return new peg$SyntaxError( + peg$SyntaxError.buildMessage(expected, found), + expected, + found, + location + ); + } + + function peg$parsesearch() { + let s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parsespaces(); + s2 = []; + s3 = peg$parseterm(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseterm(); + } + peg$savedPos = s0; + s0 = peg$f0(s1, s2); + + return s0; + } + + function peg$parseterm() { + let s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$parseboolean_operator(); + if (s1 === peg$FAILED) { + s1 = peg$parseparen_group(); + if (s1 === peg$FAILED) { + s1 = peg$parseopen_paren(); + if (s1 === peg$FAILED) { + s1 = peg$parseclosed_paren(); + if (s1 === peg$FAILED) { + s1 = peg$parsefilter(); + if (s1 === peg$FAILED) { + s1 = peg$parsefree_text(); + } + } + } + } + } + if (s1 !== peg$FAILED) { + s2 = peg$parsespaces(); + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseterm_no_paren() { + let s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$parseboolean_operator(); + if (s1 === peg$FAILED) { + s1 = peg$parseparen_group(); + if (s1 === peg$FAILED) { + s1 = peg$parsefilter(); + if (s1 === peg$FAILED) { + s1 = peg$parsefree_text(); + } + } + } + if (s1 !== peg$FAILED) { + s2 = peg$parsespaces(); + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseboolean_operator() { + let s0, s1; + + s0 = peg$currPos; + s1 = peg$parseor_operator(); + if (s1 === peg$FAILED) { + s1 = peg$parseand_operator(); + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f1(); + } + s0 = s1; + + return s0; + } + + function peg$parseparen_group() { + let s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parseopen_paren(); + if (s1 !== peg$FAILED) { + s2 = peg$parsespaces(); + s3 = []; + s4 = peg$parseterm_no_paren(); + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$parseterm_no_paren(); + } + s4 = peg$parseclosed_paren(); + if (s4 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s5 = peg$f2(s2, s3); + if (s5) { + s5 = undefined; + } else { + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f3(s2, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsefree_text() { + let s0; + + s0 = peg$parsefree_text_quoted(); + if (s0 === peg$FAILED) { + s0 = peg$parsefree_text_unquoted(); + } + + return s0; + } + + function peg$parsefree_text_unquoted() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = []; + s2 = peg$currPos; + s3 = peg$currPos; + peg$silentFails++; + s4 = peg$parsefilter_start(); + peg$silentFails--; + if (s4 === peg$FAILED) { + s3 = undefined; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + s4 = peg$currPos; + peg$silentFails++; + s5 = peg$parseboolean_operator(); + peg$silentFails--; + if (s5 === peg$FAILED) { + s4 = undefined; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + if (s4 !== peg$FAILED) { + s5 = peg$parsefree_parens(); + if (s5 === peg$FAILED) { + s5 = []; + s6 = input.charAt(peg$currPos); + if (peg$r0.test(s6)) { + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } + if (s6 !== peg$FAILED) { + while (s6 !== peg$FAILED) { + s5.push(s6); + s6 = input.charAt(peg$currPos); + if (peg$r0.test(s6)) { + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } + } + } else { + s5 = peg$FAILED; + } + } + if (s5 !== peg$FAILED) { + s6 = peg$parsespaces(); + s3 = [s3, s4, s5, s6]; + s2 = s3; + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = peg$currPos; + s3 = peg$currPos; + peg$silentFails++; + s4 = peg$parsefilter_start(); + peg$silentFails--; + if (s4 === peg$FAILED) { + s3 = undefined; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + s4 = peg$currPos; + peg$silentFails++; + s5 = peg$parseboolean_operator(); + peg$silentFails--; + if (s5 === peg$FAILED) { + s4 = undefined; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + if (s4 !== peg$FAILED) { + s5 = peg$parsefree_parens(); + if (s5 === peg$FAILED) { + s5 = []; + s6 = input.charAt(peg$currPos); + if (peg$r0.test(s6)) { + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } + if (s6 !== peg$FAILED) { + while (s6 !== peg$FAILED) { + s5.push(s6); + s6 = input.charAt(peg$currPos); + if (peg$r0.test(s6)) { + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } + } + } else { + s5 = peg$FAILED; + } + } + if (s5 !== peg$FAILED) { + s6 = peg$parsespaces(); + s3 = [s3, s4, s5, s6]; + s2 = s3; + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f4(); + } + s0 = s1; + + return s0; + } + + function peg$parsefree_text_quoted() { + let s0, s1; + + s0 = peg$currPos; + s1 = peg$parsequoted_value(); + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f5(s1); + } + s0 = s1; + + return s0; + } + + function peg$parsefree_parens() { + let s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parseopen_paren(); + if (s1 !== peg$FAILED) { + s2 = peg$parsefree_text(); + if (s2 === peg$FAILED) { + s2 = null; + } + s3 = peg$parseclosed_paren(); + if (s3 !== peg$FAILED) { + s1 = [s1, s2, s3]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsefilter_start() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$currPos; + s3 = peg$parsearray_includes_key(); + if (s3 === peg$FAILED) { + s3 = peg$parsetext_key(); + if (s3 === peg$FAILED) { + s3 = peg$parsesearch_key(); + } + } + if (s3 !== peg$FAILED) { + s4 = peg$parsesep(); + if (s4 !== peg$FAILED) { + s3 = [s3, s4]; + s2 = s3; + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + if (s2 === peg$FAILED) { + s2 = peg$parseaggregate_filter_start(); + } + if (s2 !== peg$FAILED) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseaggregate_filter_start() { + let s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$currPos; + peg$silentFails++; + s2 = peg$parseaggregate_key_start(); + peg$silentFails--; + if (s2 !== peg$FAILED) { + peg$currPos = s1; + s1 = undefined; + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s2 = peg$currPos; + peg$silentFails++; + s3 = peg$parseaggregate_filter(); + peg$silentFails--; + if (s3 !== peg$FAILED) { + peg$currPos = s2; + s2 = undefined; + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseaggregate_key_start() { + let s0, s1, s2; + + s0 = peg$currPos; + s1 = []; + s2 = input.charAt(peg$currPos); + if (peg$r1.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = input.charAt(peg$currPos); + if (peg$r1.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 40) { + s2 = peg$c0; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e2); } + } + if (s2 !== peg$FAILED) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsefilter() { + let s0; + + s0 = peg$parsedate_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parsespecific_date_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parserel_date_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parseduration_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parsesize_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parseboolean_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parsenumeric_in_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parsenumeric_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parseaggregate_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parsehas_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parseis_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parseregex_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parsearray_includes_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parsetext_in_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parsetext_filter(); + } + } + } + } + } + } + } + } + } + } + } + } + } + } + + return s0; + } + + function peg$parseaggregate_filter() { + let s0; + + s0 = peg$parseaggregate_duration_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parseaggregate_size_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parseaggregate_numeric_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parseaggregate_percentage_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parseaggregate_date_filter(); + if (s0 === peg$FAILED) { + s0 = peg$parseaggregate_rel_date_filter(); + } + } + } + } + } + + return s0; + } + + function peg$parsedate_filter() { + let s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parsesearch_key(); + if (s1 !== peg$FAILED) { + s2 = peg$parsesep(); + if (s2 !== peg$FAILED) { + s3 = peg$parseoperator(); + if (s3 !== peg$FAILED) { + s4 = peg$parseiso_8601_date_format(); + if (s4 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s5 = peg$f6(s1, s3, s4); + if (s5) { + s5 = undefined; + } else { + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f7(s1, s3, s4); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsespecific_date_filter() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$parsesearch_key(); + if (s1 !== peg$FAILED) { + s2 = peg$parsesep(); + if (s2 !== peg$FAILED) { + s3 = peg$parseiso_8601_date_format(); + if (s3 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s4 = peg$f8(s1, s3); + if (s4) { + s4 = undefined; + } else { + s4 = peg$FAILED; + } + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f9(s1, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parserel_date_filter() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$parsesearch_key(); + if (s1 !== peg$FAILED) { + s2 = peg$parsesep(); + if (s2 !== peg$FAILED) { + s3 = peg$parserel_date_format(); + if (s3 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s4 = peg$f10(s1, s3); + if (s4) { + s4 = undefined; + } else { + s4 = peg$FAILED; + } + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f11(s1, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseduration_filter() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parsesearch_key(); + if (s2 !== peg$FAILED) { + s3 = peg$parsesep(); + if (s3 !== peg$FAILED) { + s4 = peg$parseoperator(); + if (s4 === peg$FAILED) { + s4 = null; + } + s5 = peg$parseduration_format(); + if (s5 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s6 = peg$f12(s1, s2, s4, s5); + if (s6) { + s6 = undefined; + } else { + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f13(s1, s2, s4, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsesize_filter() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parsesearch_key(); + if (s2 !== peg$FAILED) { + s3 = peg$parsesep(); + if (s3 !== peg$FAILED) { + s4 = peg$parseoperator(); + if (s4 === peg$FAILED) { + s4 = null; + } + s5 = peg$parsesize_format(); + if (s5 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s6 = peg$f14(s1, s2, s4, s5); + if (s6) { + s6 = undefined; + } else { + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f15(s1, s2, s4, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseboolean_filter() { + let s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parsesearch_key(); + if (s2 !== peg$FAILED) { + s3 = peg$parsesep(); + if (s3 !== peg$FAILED) { + s4 = peg$parseboolean_value(); + if (s4 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s5 = peg$f16(s1, s2, s4); + if (s5) { + s5 = undefined; + } else { + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f17(s1, s2, s4); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsenumeric_in_filter() { + let s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parsesearch_key(); + if (s2 !== peg$FAILED) { + s3 = peg$parsesep(); + if (s3 !== peg$FAILED) { + s4 = peg$parsenumeric_in_list(); + if (s4 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s5 = peg$f18(s1, s2, s4); + if (s5) { + s5 = undefined; + } else { + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f19(s1, s2, s4); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsenumeric_filter() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parsesearch_key(); + if (s2 !== peg$FAILED) { + s3 = peg$parsesep(); + if (s3 !== peg$FAILED) { + s4 = peg$parseoperator(); + if (s4 === peg$FAILED) { + s4 = null; + } + s5 = peg$parsenumeric_value(); + if (s5 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s6 = peg$f20(s1, s2, s4, s5); + if (s6) { + s6 = undefined; + } else { + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f21(s1, s2, s4, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseaggregate_duration_filter() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parseaggregate_key(); + if (s2 !== peg$FAILED) { + s3 = peg$parsesep(); + if (s3 !== peg$FAILED) { + s4 = peg$parseoperator(); + if (s4 === peg$FAILED) { + s4 = null; + } + s5 = peg$parseduration_format(); + if (s5 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s6 = peg$f22(s1, s2, s4, s5); + if (s6) { + s6 = undefined; + } else { + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f23(s1, s2, s4, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseaggregate_size_filter() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parseaggregate_key(); + if (s2 !== peg$FAILED) { + s3 = peg$parsesep(); + if (s3 !== peg$FAILED) { + s4 = peg$parseoperator(); + if (s4 === peg$FAILED) { + s4 = null; + } + s5 = peg$parsesize_format(); + if (s5 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s6 = peg$f24(s1, s2, s4, s5); + if (s6) { + s6 = undefined; + } else { + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f25(s1, s2, s4, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseaggregate_percentage_filter() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parseaggregate_key(); + if (s2 !== peg$FAILED) { + s3 = peg$parsesep(); + if (s3 !== peg$FAILED) { + s4 = peg$parseoperator(); + if (s4 === peg$FAILED) { + s4 = null; + } + s5 = peg$parsepercentage_format(); + if (s5 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s6 = peg$f26(s1, s2, s4, s5); + if (s6) { + s6 = undefined; + } else { + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f27(s1, s2, s4, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseaggregate_numeric_filter() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parseaggregate_key(); + if (s2 !== peg$FAILED) { + s3 = peg$parsesep(); + if (s3 !== peg$FAILED) { + s4 = peg$parseoperator(); + if (s4 === peg$FAILED) { + s4 = null; + } + s5 = peg$parsenumeric_value(); + if (s5 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s6 = peg$f28(s1, s2, s4, s5); + if (s6) { + s6 = undefined; + } else { + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f29(s1, s2, s4, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseaggregate_date_filter() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parseaggregate_key(); + if (s2 !== peg$FAILED) { + s3 = peg$parsesep(); + if (s3 !== peg$FAILED) { + s4 = peg$parseoperator(); + if (s4 === peg$FAILED) { + s4 = null; + } + s5 = peg$parseiso_8601_date_format(); + if (s5 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s6 = peg$f30(s1, s2, s4, s5); + if (s6) { + s6 = undefined; + } else { + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f31(s1, s2, s4, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseaggregate_rel_date_filter() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parseaggregate_key(); + if (s2 !== peg$FAILED) { + s3 = peg$parsesep(); + if (s3 !== peg$FAILED) { + s4 = peg$parseoperator(); + if (s4 === peg$FAILED) { + s4 = null; + } + s5 = peg$parserel_date_format(); + if (s5 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s6 = peg$f32(s1, s2, s4, s5); + if (s6) { + s6 = undefined; + } else { + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f33(s1, s2, s4, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsehas_filter() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$currPos; + peg$silentFails++; + if (input.substr(peg$currPos, 4) === peg$c1) { + s3 = peg$c1; + peg$currPos += 4; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e3); } + } + peg$silentFails--; + if (s3 !== peg$FAILED) { + peg$currPos = s2; + s2 = undefined; + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s3 = peg$parsesearch_key(); + if (s3 !== peg$FAILED) { + s4 = peg$parsesep(); + if (s4 !== peg$FAILED) { + s5 = peg$parsesearch_value(); + if (s5 === peg$FAILED) { + s5 = peg$parsesearch_key(); + } + if (s5 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s6 = peg$f34(s1, s3, s5); + if (s6) { + s6 = undefined; + } else { + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f35(s1, s3, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseis_filter() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$currPos; + peg$silentFails++; + if (input.substr(peg$currPos, 3) === peg$c2) { + s3 = peg$c2; + peg$currPos += 3; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } + peg$silentFails--; + if (s3 !== peg$FAILED) { + peg$currPos = s2; + s2 = undefined; + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s3 = peg$parsesearch_key(); + if (s3 !== peg$FAILED) { + s4 = peg$parsesep(); + if (s4 !== peg$FAILED) { + s5 = peg$parsesearch_value(); + if (s5 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s6 = peg$f36(s1, s3, s5); + if (s6) { + s6 = undefined; + } else { + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f37(s1, s3, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsearray_includes_filter() { + let s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parsearray_includes_key(); + if (s2 !== peg$FAILED) { + s3 = peg$parsesep(); + if (s3 !== peg$FAILED) { + s4 = peg$parsesearch_value(); + if (s4 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s5 = peg$f38(s1, s2, s4); + if (s5) { + s5 = undefined; + } else { + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f39(s1, s2, s4); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseregex_filter() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parsearray_includes_key(); + if (s2 === peg$FAILED) { + s2 = peg$parsetext_key(); + } + if (s2 !== peg$FAILED) { + s3 = peg$parsesep(); + if (s3 !== peg$FAILED) { + s4 = peg$parseregex_value(); + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f40(s1, s2, s4); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseregex_value() { + let s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c3) { + s1 = peg$c3; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseregex_pattern(); + if (input.substr(peg$currPos, 2) === peg$c3) { + s3 = peg$c3; + peg$currPos += 2; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } + if (s3 !== peg$FAILED) { + s4 = peg$currPos; + peg$silentFails++; + s5 = peg$parseend_value(); + peg$silentFails--; + if (s5 !== peg$FAILED) { + peg$currPos = s4; + s4 = undefined; + } else { + s4 = peg$FAILED; + } + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f41(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseregex_pattern() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = []; + s2 = peg$currPos; + s3 = peg$currPos; + peg$silentFails++; + s4 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c3) { + s5 = peg$c3; + peg$currPos += 2; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } + if (s5 !== peg$FAILED) { + s6 = peg$parseend_value(); + if (s6 !== peg$FAILED) { + s5 = [s5, s6]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + peg$silentFails--; + if (s4 === peg$FAILED) { + s3 = undefined; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + s4 = input.charAt(peg$currPos); + if (peg$r2.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e6); } + } + if (s4 !== peg$FAILED) { + s3 = [s3, s4]; + s2 = s3; + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = peg$currPos; + s3 = peg$currPos; + peg$silentFails++; + s4 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c3) { + s5 = peg$c3; + peg$currPos += 2; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } + if (s5 !== peg$FAILED) { + s6 = peg$parseend_value(); + if (s6 !== peg$FAILED) { + s5 = [s5, s6]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + peg$silentFails--; + if (s4 === peg$FAILED) { + s3 = undefined; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + s4 = input.charAt(peg$currPos); + if (peg$r2.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e6); } + } + if (s4 !== peg$FAILED) { + s3 = [s3, s4]; + s2 = s3; + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } + peg$savedPos = s0; + s1 = peg$f42(); + s0 = s1; + + return s0; + } + + function peg$parsetext_in_filter() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parsetext_key(); + if (s2 !== peg$FAILED) { + s3 = peg$parsesep(); + if (s3 !== peg$FAILED) { + s4 = peg$parsewildcard_op(); + if (s4 === peg$FAILED) { + s4 = null; + } + s5 = peg$parsetext_in_list(); + if (s5 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s6 = peg$f43(s1, s2, s4, s5); + if (s6) { + s6 = undefined; + } else { + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f44(s1, s2, s4, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsetext_filter() { + let s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parsenegation(); + if (s1 === peg$FAILED) { + s1 = null; + } + s2 = peg$parsetext_key(); + if (s2 !== peg$FAILED) { + s3 = peg$parsesep(); + if (s3 !== peg$FAILED) { + s4 = peg$parsewildcard_op(); + if (s4 === peg$FAILED) { + s4 = null; + } + s5 = peg$currPos; + s6 = peg$parseoperator(); + if (s6 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s7 = peg$f45(s1, s2, s4); + if (s7) { + s7 = undefined; + } else { + s7 = peg$FAILED; + } + if (s7 !== peg$FAILED) { + s6 = [s6, s7]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + if (s5 === peg$FAILED) { + s5 = null; + } + s6 = peg$parsesearch_value(); + if (s6 !== peg$FAILED) { + peg$savedPos = peg$currPos; + s7 = peg$f46(s1, s2, s4, s5, s6); + if (s7) { + s7 = undefined; + } else { + s7 = peg$FAILED; + } + if (s7 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f47(s1, s2, s4, s5, s6); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsekey() { + let s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$currPos; + s2 = []; + s3 = input.charAt(peg$currPos); + if (peg$r1.test(s3)) { + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = input.charAt(peg$currPos); + if (peg$r1.test(s3)) { + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s1 = input.substring(s1, peg$currPos); + } else { + s1 = s2; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f48(s1); + } + s0 = s1; + + return s0; + } + + function peg$parseescaped_key() { + let s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$currPos; + s2 = []; + s3 = input.charAt(peg$currPos); + if (peg$r3.test(s3)) { + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e7); } + } + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = input.charAt(peg$currPos); + if (peg$r3.test(s3)) { + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e7); } + } + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s1 = input.substring(s1, peg$currPos); + } else { + s1 = s2; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f49(s1); + } + s0 = s1; + + return s0; + } + + function peg$parsequoted_key() { + let s0, s1, s2, s3; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 34) { + s1 = peg$c4; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e8); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseescaped_key(); + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 34) { + s3 = peg$c4; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e8); } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f50(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseexplicit_flag_key() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 5) === peg$c5) { + s1 = peg$c5; + peg$currPos += 5; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e9); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseopen_bracket(); + if (s2 !== peg$FAILED) { + s3 = peg$parseescaped_key(); + if (s3 !== peg$FAILED) { + s4 = peg$parseclosed_bracket(); + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f51(s1, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseexplicit_string_flag_key() { + let s0, s1, s2, s3, s4, s5, s6, s7, s8; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 5) === peg$c5) { + s1 = peg$c5; + peg$currPos += 5; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e9); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseopen_bracket(); + if (s2 !== peg$FAILED) { + s3 = peg$parseescaped_key(); + if (s3 !== peg$FAILED) { + s4 = peg$parsespaces(); + s5 = peg$parsecomma(); + if (s5 !== peg$FAILED) { + s6 = peg$parsespaces(); + if (input.substr(peg$currPos, 6) === peg$c6) { + s7 = peg$c6; + peg$currPos += 6; + } else { + s7 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e10); } + } + if (s7 !== peg$FAILED) { + s8 = peg$parseclosed_bracket(); + if (s8 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f52(s1, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseexplicit_number_flag_key() { + let s0, s1, s2, s3, s4, s5, s6, s7, s8; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 5) === peg$c5) { + s1 = peg$c5; + peg$currPos += 5; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e9); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseopen_bracket(); + if (s2 !== peg$FAILED) { + s3 = peg$parseescaped_key(); + if (s3 !== peg$FAILED) { + s4 = peg$parsespaces(); + s5 = peg$parsecomma(); + if (s5 !== peg$FAILED) { + s6 = peg$parsespaces(); + if (input.substr(peg$currPos, 6) === peg$c7) { + s7 = peg$c7; + peg$currPos += 6; + } else { + s7 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e11); } + } + if (s7 !== peg$FAILED) { + s8 = peg$parseclosed_bracket(); + if (s8 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f53(s1, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseexplicit_tag_key() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c8) { + s1 = peg$c8; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e12); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseopen_bracket(); + if (s2 !== peg$FAILED) { + s3 = peg$parseescaped_key(); + if (s3 !== peg$FAILED) { + s4 = peg$parseclosed_bracket(); + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f54(s1, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseexplicit_string_tag_key() { + let s0, s1, s2, s3, s4, s5, s6, s7, s8; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c8) { + s1 = peg$c8; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e12); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseopen_bracket(); + if (s2 !== peg$FAILED) { + s3 = peg$parseescaped_key(); + if (s3 !== peg$FAILED) { + s4 = peg$parsespaces(); + s5 = peg$parsecomma(); + if (s5 !== peg$FAILED) { + s6 = peg$parsespaces(); + if (input.substr(peg$currPos, 6) === peg$c6) { + s7 = peg$c6; + peg$currPos += 6; + } else { + s7 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e10); } + } + if (s7 !== peg$FAILED) { + s8 = peg$parseclosed_bracket(); + if (s8 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f55(s1, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseexplicit_number_tag_key() { + let s0, s1, s2, s3, s4, s5, s6, s7, s8; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c8) { + s1 = peg$c8; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e12); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseopen_bracket(); + if (s2 !== peg$FAILED) { + s3 = peg$parseescaped_key(); + if (s3 !== peg$FAILED) { + s4 = peg$parsespaces(); + s5 = peg$parsecomma(); + if (s5 !== peg$FAILED) { + s6 = peg$parsespaces(); + if (input.substr(peg$currPos, 6) === peg$c7) { + s7 = peg$c7; + peg$currPos += 6; + } else { + s7 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e11); } + } + if (s7 !== peg$FAILED) { + s8 = peg$parseclosed_bracket(); + if (s8 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f56(s1, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseexplicit_boolean_tag_key() { + let s0, s1, s2, s3, s4, s5, s6, s7, s8; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c8) { + s1 = peg$c8; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e12); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseopen_bracket(); + if (s2 !== peg$FAILED) { + s3 = peg$parseescaped_key(); + if (s3 !== peg$FAILED) { + s4 = peg$parsespaces(); + s5 = peg$parsecomma(); + if (s5 !== peg$FAILED) { + s6 = peg$parsespaces(); + if (input.substr(peg$currPos, 7) === peg$c9) { + s7 = peg$c9; + peg$currPos += 7; + } else { + s7 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e13); } + } + if (s7 !== peg$FAILED) { + s8 = peg$parseclosed_bracket(); + if (s8 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f57(s1, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseexplicit_array_tag_key() { + let s0, s1, s2, s3, s4, s5, s6, s7, s8; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c8) { + s1 = peg$c8; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e12); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseopen_bracket(); + if (s2 !== peg$FAILED) { + s3 = peg$parseescaped_key(); + if (s3 !== peg$FAILED) { + s4 = peg$parsespaces(); + s5 = peg$parsecomma(); + if (s5 !== peg$FAILED) { + s6 = peg$parsespaces(); + if (input.substr(peg$currPos, 5) === peg$c10) { + s7 = peg$c10; + peg$currPos += 5; + } else { + s7 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e14); } + } + if (s7 !== peg$FAILED) { + s8 = peg$parseclosed_bracket(); + if (s8 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f58(s1, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsearray_access_suffix() { + let s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parseopen_bracket(); + if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 42) { + s2 = peg$c11; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } + if (s2 !== peg$FAILED) { + s3 = peg$parseclosed_bracket(); + if (s3 !== peg$FAILED) { + s1 = [s1, s2, s3]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsearray_includes_tag_key() { + let s0, s1, s2, s3, s4, s5, s6, s7, s8, s9; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c8) { + s1 = peg$c8; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e12); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseopen_bracket(); + if (s2 !== peg$FAILED) { + s3 = peg$parseescaped_key(); + if (s3 !== peg$FAILED) { + s4 = peg$parsearray_access_suffix(); + if (s4 !== peg$FAILED) { + s5 = peg$parsespaces(); + s6 = peg$parsecomma(); + if (s6 !== peg$FAILED) { + s7 = peg$parsespaces(); + if (input.substr(peg$currPos, 5) === peg$c10) { + s8 = peg$c10; + peg$currPos += 5; + } else { + s8 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e14); } + } + if (s8 !== peg$FAILED) { + s9 = peg$parseclosed_bracket(); + if (s9 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f59(s1, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsearray_includes_attr_key() { + let s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$parsekey(); + if (s1 !== peg$FAILED) { + s2 = peg$parsearray_access_suffix(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f60(s1); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsearray_includes_key() { + let s0; + + s0 = peg$parsearray_includes_tag_key(); + if (s0 === peg$FAILED) { + s0 = peg$parsearray_includes_attr_key(); + } + + return s0; + } + + function peg$parseaggregate_key() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parsekey(); + if (s1 !== peg$FAILED) { + s2 = peg$parseopen_paren(); + if (s2 !== peg$FAILED) { + s3 = peg$parsespaces(); + s4 = peg$parsefunction_args(); + if (s4 === peg$FAILED) { + s4 = null; + } + s5 = peg$parsespaces(); + s6 = peg$parseclosed_paren(); + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f61(s1, s3, s4, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsefunction_args() { + let s0, s1, s2, s3, s4, s5, s6, s7, s8, s9; + + s0 = peg$currPos; + s1 = peg$parseaggregate_param(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parsespaces(); + s5 = peg$parsecomma(); + if (s5 !== peg$FAILED) { + s6 = peg$parsespaces(); + s7 = peg$currPos; + s8 = peg$currPos; + peg$silentFails++; + s9 = peg$parsecomma(); + peg$silentFails--; + if (s9 === peg$FAILED) { + s8 = undefined; + } else { + peg$currPos = s8; + s8 = peg$FAILED; + } + if (s8 !== peg$FAILED) { + s9 = peg$parseaggregate_param(); + if (s9 !== peg$FAILED) { + s8 = [s8, s9]; + s7 = s8; + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + if (s7 === peg$FAILED) { + s7 = null; + } + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parsespaces(); + s5 = peg$parsecomma(); + if (s5 !== peg$FAILED) { + s6 = peg$parsespaces(); + s7 = peg$currPos; + s8 = peg$currPos; + peg$silentFails++; + s9 = peg$parsecomma(); + peg$silentFails--; + if (s9 === peg$FAILED) { + s8 = undefined; + } else { + peg$currPos = s8; + s8 = peg$FAILED; + } + if (s8 !== peg$FAILED) { + s9 = peg$parseaggregate_param(); + if (s9 !== peg$FAILED) { + s8 = [s8, s9]; + s7 = s8; + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + if (s7 === peg$FAILED) { + s7 = null; + } + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } + peg$savedPos = s0; + s0 = peg$f62(s1, s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseaggregate_param() { + let s0; + + s0 = peg$parseexplicit_tag_aggregate_param(); + if (s0 === peg$FAILED) { + s0 = peg$parsequoted_aggregate_param(); + if (s0 === peg$FAILED) { + s0 = peg$parseraw_aggregate_param(); + } + } + + return s0; + } + + function peg$parseraw_aggregate_param() { + let s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$currPos; + s2 = []; + s3 = input.charAt(peg$currPos); + if (peg$r4.test(s3)) { + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e16); } + } + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = input.charAt(peg$currPos); + if (peg$r4.test(s3)) { + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e16); } + } + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s1 = input.substring(s1, peg$currPos); + } else { + s1 = s2; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f63(s1); + } + s0 = s1; + + return s0; + } + + function peg$parsequoted_aggregate_param() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 34) { + s1 = peg$c4; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e8); } + } + if (s1 !== peg$FAILED) { + s2 = peg$currPos; + s3 = []; + if (input.substr(peg$currPos, 2) === peg$c12) { + s4 = peg$c12; + peg$currPos += 2; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e17); } + } + if (s4 === peg$FAILED) { + s4 = input.charAt(peg$currPos); + if (peg$r5.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e18); } + } + } + while (s4 !== peg$FAILED) { + s3.push(s4); + if (input.substr(peg$currPos, 2) === peg$c12) { + s4 = peg$c12; + peg$currPos += 2; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e17); } + } + if (s4 === peg$FAILED) { + s4 = input.charAt(peg$currPos); + if (peg$r5.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e18); } + } + } + } + s2 = input.substring(s2, peg$currPos); + if (input.charCodeAt(peg$currPos) === 34) { + s3 = peg$c4; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e8); } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f64(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseexplicit_tag_aggregate_param() { + let s0, s1; + + s0 = peg$currPos; + s1 = peg$parseexplicit_tag_key(); + if (s1 === peg$FAILED) { + s1 = peg$parseexplicit_string_tag_key(); + if (s1 === peg$FAILED) { + s1 = peg$parseexplicit_number_tag_key(); + if (s1 === peg$FAILED) { + s1 = peg$parseexplicit_boolean_tag_key(); + } + } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f65(s1); + } + s0 = s1; + + return s0; + } + + function peg$parsesearch_key() { + let s0; + + s0 = peg$parseexplicit_number_flag_key(); + if (s0 === peg$FAILED) { + s0 = peg$parseexplicit_number_tag_key(); + if (s0 === peg$FAILED) { + s0 = peg$parseexplicit_boolean_tag_key(); + if (s0 === peg$FAILED) { + s0 = peg$parsekey(); + if (s0 === peg$FAILED) { + s0 = peg$parsequoted_key(); + } + } + } + } + + return s0; + } + + function peg$parsetext_key() { + let s0; + + s0 = peg$parseexplicit_flag_key(); + if (s0 === peg$FAILED) { + s0 = peg$parseexplicit_string_flag_key(); + if (s0 === peg$FAILED) { + s0 = peg$parseexplicit_tag_key(); + if (s0 === peg$FAILED) { + s0 = peg$parseexplicit_string_tag_key(); + if (s0 === peg$FAILED) { + s0 = peg$parseexplicit_array_tag_key(); + if (s0 === peg$FAILED) { + s0 = peg$parsesearch_key(); + } + } + } + } + } + + return s0; + } + + function peg$parsevalue() { + let s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$currPos; + s2 = []; + s3 = input.charAt(peg$currPos); + if (peg$r6.test(s3)) { + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e19); } + } + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = input.charAt(peg$currPos); + if (peg$r6.test(s3)) { + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e19); } + } + } + s1 = input.substring(s1, peg$currPos); + peg$savedPos = s0; + s1 = peg$f66(s1); + s0 = s1; + + return s0; + } + + function peg$parsequoted_value() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 34) { + s1 = peg$c4; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e8); } + } + if (s1 !== peg$FAILED) { + s2 = peg$currPos; + s3 = []; + if (input.substr(peg$currPos, 2) === peg$c12) { + s4 = peg$c12; + peg$currPos += 2; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e17); } + } + if (s4 === peg$FAILED) { + s4 = input.charAt(peg$currPos); + if (peg$r7.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e20); } + } + } + while (s4 !== peg$FAILED) { + s3.push(s4); + if (input.substr(peg$currPos, 2) === peg$c12) { + s4 = peg$c12; + peg$currPos += 2; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e17); } + } + if (s4 === peg$FAILED) { + s4 = input.charAt(peg$currPos); + if (peg$r7.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e20); } + } + } + } + s2 = input.substring(s2, peg$currPos); + if (input.charCodeAt(peg$currPos) === 34) { + s3 = peg$c4; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e8); } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f67(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsein_value() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = []; + s2 = peg$currPos; + s3 = peg$currPos; + peg$silentFails++; + s4 = peg$parsein_value_termination(); + peg$silentFails--; + if (s4 !== peg$FAILED) { + peg$currPos = s3; + s3 = undefined; + } else { + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + s4 = peg$parsein_value_char(); + if (s4 !== peg$FAILED) { + s3 = [s3, s4]; + s2 = s3; + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = peg$currPos; + s3 = peg$currPos; + peg$silentFails++; + s4 = peg$parsein_value_termination(); + peg$silentFails--; + if (s4 !== peg$FAILED) { + peg$currPos = s3; + s3 = undefined; + } else { + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + s4 = peg$parsein_value_char(); + if (s4 !== peg$FAILED) { + s3 = [s3, s4]; + s2 = s3; + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f68(); + } + s0 = s1; + + return s0; + } + + function peg$parsetext_in_value() { + let s0; + + s0 = peg$parsequoted_value(); + if (s0 === peg$FAILED) { + s0 = peg$parsein_value(); + } + + return s0; + } + + function peg$parsesearch_value() { + let s0; + + s0 = peg$parsequoted_value(); + if (s0 === peg$FAILED) { + s0 = peg$parsevalue(); + } + + return s0; + } + + function peg$parsenumeric_value() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 45) { + s2 = peg$c13; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e21); } + } + if (s2 === peg$FAILED) { + s2 = null; + } + s3 = peg$parsenumeric(); + if (s3 !== peg$FAILED) { + s2 = [s2, s3]; + s1 = s2; + } else { + peg$currPos = s1; + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s2 = peg$parsenumber_unit(); + if (s2 === peg$FAILED) { + s2 = null; + } + s3 = peg$currPos; + peg$silentFails++; + s4 = peg$parseend_value(); + if (s4 === peg$FAILED) { + s4 = input.charAt(peg$currPos); + if (peg$r8.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e22); } + } + } + peg$silentFails--; + if (s4 !== peg$FAILED) { + peg$currPos = s3; + s3 = undefined; + } else { + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f69(s1, s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseboolean_value() { + let s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = input.substr(peg$currPos, 4); + if (s1.toLowerCase() === peg$c14) { + peg$currPos += (4); + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e23); } + } + if (s1 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 49) { + s1 = peg$c15; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e24); } + } + if (s1 === peg$FAILED) { + s1 = input.substr(peg$currPos, 5); + if (s1.toLowerCase() === peg$c16) { + peg$currPos += (5); + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e25); } + } + if (s1 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 48) { + s1 = peg$c17; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e26); } + } + } + } + } + if (s1 !== peg$FAILED) { + s2 = peg$currPos; + peg$silentFails++; + s3 = peg$parseend_value(); + peg$silentFails--; + if (s3 !== peg$FAILED) { + peg$currPos = s2; + s2 = undefined; + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f70(s1); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsetext_in_list() { + let s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10; + + s0 = peg$currPos; + s1 = peg$parseopen_bracket(); + if (s1 !== peg$FAILED) { + s2 = peg$parsetext_in_value(); + if (s2 !== peg$FAILED) { + s3 = []; + s4 = peg$currPos; + s5 = peg$parsespaces(); + s6 = peg$parsecomma(); + if (s6 !== peg$FAILED) { + s7 = peg$parsespaces(); + s8 = peg$currPos; + s9 = peg$currPos; + peg$silentFails++; + s10 = peg$parsecomma(); + peg$silentFails--; + if (s10 === peg$FAILED) { + s9 = undefined; + } else { + peg$currPos = s9; + s9 = peg$FAILED; + } + if (s9 !== peg$FAILED) { + s10 = peg$parsetext_in_value(); + if (s10 !== peg$FAILED) { + s9 = [s9, s10]; + s8 = s9; + } else { + peg$currPos = s8; + s8 = peg$FAILED; + } + } else { + peg$currPos = s8; + s8 = peg$FAILED; + } + if (s8 === peg$FAILED) { + s8 = null; + } + s5 = [s5, s6, s7, s8]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$currPos; + s5 = peg$parsespaces(); + s6 = peg$parsecomma(); + if (s6 !== peg$FAILED) { + s7 = peg$parsespaces(); + s8 = peg$currPos; + s9 = peg$currPos; + peg$silentFails++; + s10 = peg$parsecomma(); + peg$silentFails--; + if (s10 === peg$FAILED) { + s9 = undefined; + } else { + peg$currPos = s9; + s9 = peg$FAILED; + } + if (s9 !== peg$FAILED) { + s10 = peg$parsetext_in_value(); + if (s10 !== peg$FAILED) { + s9 = [s9, s10]; + s8 = s9; + } else { + peg$currPos = s8; + s8 = peg$FAILED; + } + } else { + peg$currPos = s8; + s8 = peg$FAILED; + } + if (s8 === peg$FAILED) { + s8 = null; + } + s5 = [s5, s6, s7, s8]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } + s4 = peg$parseclosed_bracket(); + if (s4 !== peg$FAILED) { + s5 = peg$currPos; + peg$silentFails++; + s6 = peg$parseend_value(); + peg$silentFails--; + if (s6 !== peg$FAILED) { + peg$currPos = s5; + s5 = undefined; + } else { + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f71(s2, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsenumeric_in_list() { + let s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10; + + s0 = peg$currPos; + s1 = peg$parseopen_bracket(); + if (s1 !== peg$FAILED) { + s2 = peg$parsenumeric_value(); + if (s2 !== peg$FAILED) { + s3 = []; + s4 = peg$currPos; + s5 = peg$parsespaces(); + s6 = peg$parsecomma(); + if (s6 !== peg$FAILED) { + s7 = peg$parsespaces(); + s8 = peg$currPos; + s9 = peg$currPos; + peg$silentFails++; + s10 = peg$parsecomma(); + peg$silentFails--; + if (s10 === peg$FAILED) { + s9 = undefined; + } else { + peg$currPos = s9; + s9 = peg$FAILED; + } + if (s9 !== peg$FAILED) { + s10 = peg$parsenumeric_value(); + if (s10 !== peg$FAILED) { + s9 = [s9, s10]; + s8 = s9; + } else { + peg$currPos = s8; + s8 = peg$FAILED; + } + } else { + peg$currPos = s8; + s8 = peg$FAILED; + } + if (s8 === peg$FAILED) { + s8 = null; + } + s5 = [s5, s6, s7, s8]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$currPos; + s5 = peg$parsespaces(); + s6 = peg$parsecomma(); + if (s6 !== peg$FAILED) { + s7 = peg$parsespaces(); + s8 = peg$currPos; + s9 = peg$currPos; + peg$silentFails++; + s10 = peg$parsecomma(); + peg$silentFails--; + if (s10 === peg$FAILED) { + s9 = undefined; + } else { + peg$currPos = s9; + s9 = peg$FAILED; + } + if (s9 !== peg$FAILED) { + s10 = peg$parsenumeric_value(); + if (s10 !== peg$FAILED) { + s9 = [s9, s10]; + s8 = s9; + } else { + peg$currPos = s8; + s8 = peg$FAILED; + } + } else { + peg$currPos = s8; + s8 = peg$FAILED; + } + if (s8 === peg$FAILED) { + s8 = null; + } + s5 = [s5, s6, s7, s8]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } + s4 = peg$parseclosed_bracket(); + if (s4 !== peg$FAILED) { + s5 = peg$currPos; + peg$silentFails++; + s6 = peg$parseend_value(); + peg$silentFails--; + if (s6 !== peg$FAILED) { + peg$currPos = s5; + s5 = undefined; + } else { + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f72(s2, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsewildcard_op() { + let s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parsewildcard_unicode(); + if (s1 !== peg$FAILED) { + s2 = peg$parsecontains(); + if (s2 === peg$FAILED) { + s2 = peg$parsestarts_with(); + if (s2 === peg$FAILED) { + s2 = peg$parseends_with(); + } + } + if (s2 !== peg$FAILED) { + s3 = peg$parsewildcard_unicode(); + if (s3 !== peg$FAILED) { + s1 = [s1, s2, s3]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsein_value_termination() { + let s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parsein_value_char(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$currPos; + s4 = peg$currPos; + peg$silentFails++; + s5 = peg$parsein_value_end(); + peg$silentFails--; + if (s5 === peg$FAILED) { + s4 = undefined; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + if (s4 !== peg$FAILED) { + s5 = peg$parsein_value_char(); + if (s5 !== peg$FAILED) { + s4 = [s4, s5]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$currPos; + peg$silentFails++; + s5 = peg$parsein_value_end(); + peg$silentFails--; + if (s5 === peg$FAILED) { + s4 = undefined; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + if (s4 !== peg$FAILED) { + s5 = peg$parsein_value_char(); + if (s5 !== peg$FAILED) { + s4 = [s4, s5]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } + s3 = peg$parsein_value_end(); + if (s3 !== peg$FAILED) { + s1 = [s1, s2, s3]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsein_value_char() { + let s0; + + s0 = input.charAt(peg$currPos); + if (peg$r9.test(s0)) { + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e27); } + } + + return s0; + } + + function peg$parsein_value_end() { + let s0, s1, s2; + + s0 = peg$parseclosed_bracket(); + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parsespaces(); + s2 = peg$parsecomma(); + if (s2 !== peg$FAILED) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } + + return s0; + } + + function peg$parsenum2() { + let s0, s1, s2; + + s0 = peg$currPos; + s1 = input.charAt(peg$currPos); + if (peg$r10.test(s1)) { + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s1 !== peg$FAILED) { + s2 = input.charAt(peg$currPos); + if (peg$r10.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s2 !== peg$FAILED) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsenum4() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = input.charAt(peg$currPos); + if (peg$r10.test(s1)) { + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s1 !== peg$FAILED) { + s2 = input.charAt(peg$currPos); + if (peg$r10.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s2 !== peg$FAILED) { + s3 = input.charAt(peg$currPos); + if (peg$r10.test(s3)) { + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s3 !== peg$FAILED) { + s4 = input.charAt(peg$currPos); + if (peg$r10.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s4 !== peg$FAILED) { + s1 = [s1, s2, s3, s4]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsedate_format() { + let s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parsenum4(); + if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 45) { + s2 = peg$c13; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e21); } + } + if (s2 !== peg$FAILED) { + s3 = peg$parsenum2(); + if (s3 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 45) { + s4 = peg$c13; + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e21); } + } + if (s4 !== peg$FAILED) { + s5 = peg$parsenum2(); + if (s5 !== peg$FAILED) { + s1 = [s1, s2, s3, s4, s5]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsetime_format() { + let s0, s1, s2, s3, s4, s5, s6, s7, s8, s9; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 84) { + s1 = peg$c18; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e29); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parsenum2(); + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 58) { + s3 = peg$c19; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e30); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parsenum2(); + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 58) { + s5 = peg$c19; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e30); } + } + if (s5 !== peg$FAILED) { + s6 = peg$parsenum2(); + if (s6 !== peg$FAILED) { + s7 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 46) { + s8 = peg$c20; + peg$currPos++; + } else { + s8 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e31); } + } + if (s8 !== peg$FAILED) { + s9 = peg$parsems_format(); + if (s9 !== peg$FAILED) { + s8 = [s8, s9]; + s7 = s8; + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + if (s7 === peg$FAILED) { + s7 = null; + } + s1 = [s1, s2, s3, s4, s5, s6, s7]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsems_format() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = input.charAt(peg$currPos); + if (peg$r10.test(s1)) { + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s1 !== peg$FAILED) { + s2 = input.charAt(peg$currPos); + if (peg$r10.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s2 === peg$FAILED) { + s2 = null; + } + s3 = input.charAt(peg$currPos); + if (peg$r10.test(s3)) { + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s3 === peg$FAILED) { + s3 = null; + } + s4 = input.charAt(peg$currPos); + if (peg$r10.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s4 === peg$FAILED) { + s4 = null; + } + s5 = input.charAt(peg$currPos); + if (peg$r10.test(s5)) { + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s5 === peg$FAILED) { + s5 = null; + } + s6 = input.charAt(peg$currPos); + if (peg$r10.test(s6)) { + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s6 === peg$FAILED) { + s6 = null; + } + s1 = [s1, s2, s3, s4, s5, s6]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsetz_format() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = input.charAt(peg$currPos); + if (peg$r11.test(s1)) { + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e32); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parsenum2(); + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 58) { + s3 = peg$c19; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e30); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parsenum2(); + if (s4 !== peg$FAILED) { + s1 = [s1, s2, s3, s4]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseiso_8601_date_format() { + let s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parsedate_format(); + if (s1 !== peg$FAILED) { + s2 = peg$parsetime_format(); + if (s2 === peg$FAILED) { + s2 = null; + } + if (input.charCodeAt(peg$currPos) === 90) { + s3 = peg$c21; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e33); } + } + if (s3 === peg$FAILED) { + s3 = peg$parsetz_format(); + } + if (s3 === peg$FAILED) { + s3 = null; + } + s4 = peg$currPos; + peg$silentFails++; + s5 = peg$parseend_value(); + peg$silentFails--; + if (s5 !== peg$FAILED) { + peg$currPos = s4; + s4 = undefined; + } else { + s4 = peg$FAILED; + } + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f73(s1, s2, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parserel_date_format() { + let s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = input.charAt(peg$currPos); + if (peg$r11.test(s1)) { + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e32); } + } + if (s1 !== peg$FAILED) { + s2 = peg$currPos; + s3 = []; + s4 = input.charAt(peg$currPos); + if (peg$r10.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s4 !== peg$FAILED) { + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = input.charAt(peg$currPos); + if (peg$r10.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + } + } else { + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + s2 = input.substring(s2, peg$currPos); + } else { + s2 = s3; + } + if (s2 !== peg$FAILED) { + s3 = input.charAt(peg$currPos); + if (peg$r12.test(s3)) { + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e34); } + } + if (s3 !== peg$FAILED) { + s4 = peg$currPos; + peg$silentFails++; + s5 = peg$parseend_value(); + peg$silentFails--; + if (s5 !== peg$FAILED) { + peg$currPos = s4; + s4 = undefined; + } else { + s4 = peg$FAILED; + } + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f74(s1, s2, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseduration_format() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$parsenumeric(); + if (s1 !== peg$FAILED) { + s2 = peg$parseduration_unit(); + if (s2 !== peg$FAILED) { + s3 = peg$currPos; + peg$silentFails++; + s4 = peg$parseend_value(); + peg$silentFails--; + if (s4 !== peg$FAILED) { + peg$currPos = s3; + s3 = undefined; + } else { + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f75(s1, s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsesize_format() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$parsenumeric(); + if (s1 !== peg$FAILED) { + s2 = peg$parsesize_unit(); + if (s2 !== peg$FAILED) { + s3 = peg$currPos; + peg$silentFails++; + s4 = peg$parseend_value(); + peg$silentFails--; + if (s4 !== peg$FAILED) { + peg$currPos = s3; + s3 = undefined; + } else { + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f76(s1, s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsepercentage_format() { + let s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$parsenumeric(); + if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 37) { + s2 = peg$c22; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e35); } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f77(s1); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsenumber_unit() { + let s0; + + s0 = input.charAt(peg$currPos); + if (peg$r13.test(s0)) { + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e36); } + } + + return s0; + } + + function peg$parseduration_unit() { + let s0; + + if (input.substr(peg$currPos, 2) === peg$c23) { + s0 = peg$c23; + peg$currPos += 2; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e37); } + } + if (s0 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 115) { + s0 = peg$c24; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e38); } + } + if (s0 === peg$FAILED) { + if (input.substr(peg$currPos, 3) === peg$c25) { + s0 = peg$c25; + peg$currPos += 3; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e39); } + } + if (s0 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 109) { + s0 = peg$c26; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e40); } + } + if (s0 === peg$FAILED) { + if (input.substr(peg$currPos, 2) === peg$c27) { + s0 = peg$c27; + peg$currPos += 2; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e41); } + } + if (s0 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 104) { + s0 = peg$c28; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e42); } + } + if (s0 === peg$FAILED) { + if (input.substr(peg$currPos, 3) === peg$c29) { + s0 = peg$c29; + peg$currPos += 3; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e43); } + } + if (s0 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 100) { + s0 = peg$c30; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e44); } + } + if (s0 === peg$FAILED) { + if (input.substr(peg$currPos, 2) === peg$c31) { + s0 = peg$c31; + peg$currPos += 2; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e45); } + } + if (s0 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 119) { + s0 = peg$c32; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e46); } + } + } + } + } + } + } + } + } + } + } + + return s0; + } + + function peg$parsesize_unit() { + let s0; + + s0 = peg$parsebit_unit(); + if (s0 === peg$FAILED) { + s0 = peg$parsebyte_unit(); + } + + return s0; + } + + function peg$parsebit_unit() { + let s0; + + s0 = input.substr(peg$currPos, 3); + if (s0.toLowerCase() === peg$c33) { + peg$currPos += (3); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e47); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 3); + if (s0.toLowerCase() === peg$c34) { + peg$currPos += (3); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e48); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 3); + if (s0.toLowerCase() === peg$c35) { + peg$currPos += (3); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e49); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 3); + if (s0.toLowerCase() === peg$c36) { + peg$currPos += (3); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e50); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 3); + if (s0.toLowerCase() === peg$c37) { + peg$currPos += (3); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e51); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 3); + if (s0.toLowerCase() === peg$c38) { + peg$currPos += (3); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e52); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 3); + if (s0.toLowerCase() === peg$c39) { + peg$currPos += (3); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e53); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 3); + if (s0.toLowerCase() === peg$c40) { + peg$currPos += (3); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e54); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 3); + if (s0.toLowerCase() === peg$c41) { + peg$currPos += (3); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e55); } + } + } + } + } + } + } + } + } + } + + return s0; + } + + function peg$parsebyte_unit() { + let s0; + + s0 = input.substr(peg$currPos, 5); + if (s0.toLowerCase() === peg$c42) { + peg$currPos += (5); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e56); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 2); + if (s0.toLowerCase() === peg$c43) { + peg$currPos += (2); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e57); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 2); + if (s0.toLowerCase() === peg$c44) { + peg$currPos += (2); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e58); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 2); + if (s0.toLowerCase() === peg$c45) { + peg$currPos += (2); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e59); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 2); + if (s0.toLowerCase() === peg$c46) { + peg$currPos += (2); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e60); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 2); + if (s0.toLowerCase() === peg$c47) { + peg$currPos += (2); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e61); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 2); + if (s0.toLowerCase() === peg$c48) { + peg$currPos += (2); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e62); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 2); + if (s0.toLowerCase() === peg$c49) { + peg$currPos += (2); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e63); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 2); + if (s0.toLowerCase() === peg$c50) { + peg$currPos += (2); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e64); } + } + if (s0 === peg$FAILED) { + s0 = input.substr(peg$currPos, 2); + if (s0.toLowerCase() === peg$c51) { + peg$currPos += (2); + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e65); } + } + } + } + } + } + } + } + } + } + } + + return s0; + } + + function peg$parseoperator() { + let s0; + + if (input.substr(peg$currPos, 2) === peg$c52) { + s0 = peg$c52; + peg$currPos += 2; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e66); } + } + if (s0 === peg$FAILED) { + if (input.substr(peg$currPos, 2) === peg$c53) { + s0 = peg$c53; + peg$currPos += 2; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e67); } + } + if (s0 === peg$FAILED) { + s0 = input.charAt(peg$currPos); + if (peg$r14.test(s0)) { + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e68); } + } + if (s0 === peg$FAILED) { + if (input.substr(peg$currPos, 2) === peg$c54) { + s0 = peg$c54; + peg$currPos += 2; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e69); } + } + } + } + } + + return s0; + } + + function peg$parseor_operator() { + let s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = input.substr(peg$currPos, 2); + if (s1.toLowerCase() === peg$c55) { + peg$currPos += (2); + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e70); } + } + if (s1 !== peg$FAILED) { + s2 = peg$currPos; + peg$silentFails++; + s3 = peg$parseend_value(); + peg$silentFails--; + if (s3 !== peg$FAILED) { + peg$currPos = s2; + s2 = undefined; + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseand_operator() { + let s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = input.substr(peg$currPos, 3); + if (s1.toLowerCase() === peg$c56) { + peg$currPos += (3); + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e71); } + } + if (s1 !== peg$FAILED) { + s2 = peg$currPos; + peg$silentFails++; + s3 = peg$parseend_value(); + peg$silentFails--; + if (s3 !== peg$FAILED) { + peg$currPos = s2; + s2 = undefined; + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsenumeric() { + let s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = []; + s2 = input.charAt(peg$currPos); + if (peg$r10.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = input.charAt(peg$currPos); + if (peg$r10.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s2 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 46) { + s3 = peg$c20; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e31); } + } + if (s3 !== peg$FAILED) { + s4 = []; + s5 = input.charAt(peg$currPos); + if (peg$r10.test(s5)) { + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + while (s5 !== peg$FAILED) { + s4.push(s5); + s5 = input.charAt(peg$currPos); + if (peg$r10.test(s5)) { + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + } + s3 = [s3, s4]; + s2 = s3; + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + if (s2 === peg$FAILED) { + s2 = null; + } + peg$savedPos = s0; + s0 = peg$f78(); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseopen_paren() { + let s0, s1; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 40) { + s1 = peg$c0; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e2); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f79(); + } + s0 = s1; + + return s0; + } + + function peg$parseclosed_paren() { + let s0, s1; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 41) { + s1 = peg$c57; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e72); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f80(); + } + s0 = s1; + + return s0; + } + + function peg$parseopen_bracket() { + let s0; + + if (input.charCodeAt(peg$currPos) === 91) { + s0 = peg$c58; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e73); } + } + + return s0; + } + + function peg$parseclosed_bracket() { + let s0; + + if (input.charCodeAt(peg$currPos) === 93) { + s0 = peg$c59; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e74); } + } + + return s0; + } + + function peg$parsesep() { + let s0; + + if (input.charCodeAt(peg$currPos) === 58) { + s0 = peg$c19; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e30); } + } + + return s0; + } + + function peg$parsenegation() { + let s0; + + if (input.charCodeAt(peg$currPos) === 33) { + s0 = peg$c60; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e75); } + } + + return s0; + } + + function peg$parsewildcard_unicode() { + let s0; + + s0 = input.charAt(peg$currPos); + if (peg$r15.test(s0)) { + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e76); } + } + + return s0; + } + + function peg$parsecontains() { + let s0; + + if (input.substr(peg$currPos, 8) === peg$c61) { + s0 = peg$c61; + peg$currPos += 8; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e77); } + } + + return s0; + } + + function peg$parsestarts_with() { + let s0; + + if (input.substr(peg$currPos, 10) === peg$c62) { + s0 = peg$c62; + peg$currPos += 10; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e78); } + } + + return s0; + } + + function peg$parseends_with() { + let s0; + + if (input.substr(peg$currPos, 8) === peg$c63) { + s0 = peg$c63; + peg$currPos += 8; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e79); } + } + + return s0; + } + + function peg$parsecomma() { + let s0; + + if (input.charCodeAt(peg$currPos) === 44) { + s0 = peg$c64; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e80); } + } + + return s0; + } + + function peg$parsespaces() { + let s0, s1, s2; + + s0 = peg$currPos; + s1 = []; + if (input.charCodeAt(peg$currPos) === 32) { + s2 = peg$c65; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e81); } + } + while (s2 !== peg$FAILED) { + s1.push(s2); + if (input.charCodeAt(peg$currPos) === 32) { + s2 = peg$c65; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e81); } + } + } + peg$savedPos = s0; + s1 = peg$f81(); + s0 = s1; + + return s0; + } + + function peg$parseend_value() { + let s0, s1; + + s0 = input.charAt(peg$currPos); + if (peg$r16.test(s0)) { + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e82); } + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + peg$silentFails++; + if (input.length > peg$currPos) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e83); } + } + peg$silentFails--; + if (s1 === peg$FAILED) { + s0 = undefined; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } + + return s0; + } + + + const {TokenConverter, TermOperator, FilterType, config} = options; + const tc = new TokenConverter({text, location, config}); + + const opDefault = TermOperator.DEFAULT; + + peg$result = peg$startRuleFunction(); + + const peg$success = (peg$result !== peg$FAILED && peg$currPos === input.length); + function peg$throw() { + if (peg$result !== peg$FAILED && peg$currPos < input.length) { + peg$fail(peg$endExpectation()); + } + + throw peg$buildStructuredError( + peg$maxFailExpected, + peg$maxFailPos < input.length ? peg$getUnicode(peg$maxFailPos) : null, + peg$maxFailPos < input.length + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ); + } + if (options.peg$library) { + return /** @type {any} */ ({ + peg$result, + peg$currPos, + peg$FAILED, + peg$maxFailExpected, + peg$maxFailPos, + peg$success, + peg$throw: peg$success ? undefined : peg$throw, + }); + } + if (peg$success) { + return peg$result; + } else { + peg$throw(); + } +} + +const peg$allowedStartRules = [ + "search" +]; + +export { + peg$allowedStartRules as StartRules, + peg$SyntaxError as SyntaxError, + peg$parse as parse +}; + diff --git a/packages/toolkit-core/src/search-syntax.test.ts b/packages/toolkit-core/src/search-syntax.test.ts new file mode 100644 index 000000000..7df2a1516 --- /dev/null +++ b/packages/toolkit-core/src/search-syntax.test.ts @@ -0,0 +1,179 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { + FilterType, + getKeyName, + type FilterToken, + type KeyToken, + parseSearch, + parseSearchOrThrow, + type SearchConfig, + TermOperator, + Token, + TokenConverter, +} from "./search-syntax"; + +// Copied from getsentry/sentry `fixtures/search-syntax`. The frontend parser +// and the backend `event_search.py` grammar both test against these. +const FIXTURES_DIR = resolve( + dirname(fileURLToPath(import.meta.url)), + "../test/fixtures/search-syntax", +); + +type FixtureCase = { + query: string; + result: unknown[]; + additionalConfig?: Partial & Record; + raisesError?: boolean; +}; + +// Location, text, `parsed` values, and validation results are frontend +// concerns the structural converter does not produce; compare everything else. +const STRIPPED_KEYS = new Set([ + "location", + "text", + "config", + "parsed", + "invalid", + "warning", +]); + +function normalize(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(normalize); + } + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value) + .filter(([key]) => !STRIPPED_KEYS.has(key)) + .map(([key, inner]) => [key, normalize(inner)]), + ); + } + return value; +} + +const fixtureFiles = readdirSync(FIXTURES_DIR).filter((file) => + file.endsWith(".json"), +); + +describe("search-syntax fixtures", () => { + it("loads the shared fixtures", () => { + expect(fixtureFiles.length).toBeGreaterThan(70); + }); + + for (const file of fixtureFiles) { + const cases = JSON.parse( + readFileSync(join(FIXTURES_DIR, file), "utf-8"), + ) as FixtureCase[]; + + describe(file.replace(/\.json$/, ""), () => { + for (const testCase of cases) { + it(`handles ${JSON.stringify(testCase.query)}`, () => { + const result = parseSearch(testCase.query, { + config: testCase.additionalConfig, + }); + if (testCase.raisesError) { + expect(result).toBeNull(); + return; + } + expect(result).not.toBeNull(); + expect(normalize(result)).toEqual(normalize(testCase.result)); + }); + } + }); + } +}); + +describe("parseSearch", () => { + it("keeps exact text and offsets on every token", () => { + const query = "!level:error count():>10 (a OR b)"; + const result = parseSearchOrThrow(query); + expect(result.map((token) => token.text).join("")).toBe(query); + for (const token of result) { + expect( + query.slice(token.location.start.offset, token.location.end.offset), + ).toBe(token.text); + } + }); + + it("classifies typed filters from the default key sets", () => { + const result = parseSearchOrThrow( + "transaction.duration:>1s error.handled:true timestamp:-24h count():>10", + ); + const filters = result.filter( + (token): token is FilterToken => token.type === Token.FILTER, + ); + expect(filters.map((filter) => filter.filter)).toEqual([ + FilterType.DURATION, + FilterType.BOOLEAN, + FilterType.RELATIVE_DATE, + FilterType.AGGREGATE_NUMERIC, + ]); + expect(filters[0]?.operator).toBe(TermOperator.GREATER_THAN); + }); + + it("returns null for queries that do not parse, and throws from parseSearchOrThrow", () => { + // The grammar falls back to free text for almost anything; a newline + // outside quotes is one of the few inputs it rejects. + expect(parseSearch("release:a\nrelease")).toBeNull(); + expect(() => parseSearchOrThrow("release:a\nrelease")).toThrow(); + }); + + it("builds paren tokens instead of groups with flattenParenGroups", () => { + const grouped = parseSearchOrThrow("(a:b)"); + expect(grouped.map((token) => token.type)).toContain(Token.LOGIC_GROUP); + const flat = parseSearchOrThrow("(a:b)", { + config: { flattenParenGroups: true }, + }); + expect(flat.map((token) => token.type)).toEqual([ + Token.SPACES, + Token.L_PAREN, + Token.SPACES, + Token.FILTER, + Token.SPACES, + Token.R_PAREN, + Token.SPACES, + ]); + }); + + it("uses a converter subclass for validation", () => { + class NoFreeText extends TokenConverter { + override checkInvalidFreeText(value: string) { + return { type: "free-text-not-allowed", reason: `no "${value}"` }; + } + } + const result = parseSearchOrThrow("hello", { converter: NoFreeText }); + const freeText = result.find((token) => token.type === Token.FREE_TEXT); + expect(freeText).toMatchObject({ + invalid: { type: "free-text-not-allowed", reason: 'no "hello"' }, + }); + }); +}); + +describe("getKeyName", () => { + function firstKey(query: string): KeyToken { + const filter = parseSearchOrThrow(query).find( + (token): token is FilterToken => token.type === Token.FILTER, + ); + if (!filter) { + throw new Error(`no filter in ${query}`); + } + return filter.key; + } + + it("names each key kind the way the API expects", () => { + expect(getKeyName(firstKey("level:error"))).toBe("level"); + expect(getKeyName(firstKey("tags[foo]:bar"))).toBe("foo"); + expect(getKeyName(firstKey("tags[foo,number]:1"))).toBe("tags[foo,number]"); + expect(getKeyName(firstKey("flags[beta]:on"))).toBe("flags[beta]"); + expect(getKeyName(firstKey("foo[*]:bar"))).toBe("foo"); + expect(getKeyName(firstKey("count():>1"))).toBe("count"); + expect( + getKeyName(firstKey("p95(span.duration):>1"), { + aggregateWithArgs: true, + }), + ).toBe("p95(span.duration)"); + }); +}); diff --git a/packages/toolkit-core/src/search-syntax.ts b/packages/toolkit-core/src/search-syntax.ts new file mode 100644 index 000000000..6fe24c6f2 --- /dev/null +++ b/packages/toolkit-core/src/search-syntax.ts @@ -0,0 +1,1015 @@ +/** + * Sentry search syntax parser. + * + * Runs the Sentry frontend's search grammar (`grammar/search-syntax.pegjs`, + * copied unchanged from getsentry/sentry + * `static/app/components/searchSyntax/grammar.pegjs`) and builds the same + * token tree as the frontend's `parseSearch`. Location, text, and filter + * classification match; the shared fixtures in `test/fixtures/search-syntax` + * check that. + * + * {@link TokenConverter} only does structural work. It never marks a token + * invalid, attaches warnings, or fills `parsed` values. Products that need + * that (the frontend's validation messages, for example) subclass it and + * override the `checkInvalid*` hooks or the value builders. + */ + +import { parse as parseGrammar } from "./generated/search-syntax-grammar.js"; + +/** A node type in the search syntax tree. Values match the frontend. */ +export const Token = { + SPACES: "spaces", + FILTER: "filter", + FREE_TEXT: "freeText", + LOGIC_GROUP: "logicGroup", + LOGIC_BOOLEAN: "logicBoolean", + KEY_SIMPLE: "keySimple", + KEY_EXPLICIT_FLAG: "keyExplicitFlag", + KEY_EXPLICIT_NUMBER_FLAG: "keyExplicitNumberFlag", + KEY_EXPLICIT_STRING_FLAG: "keyExplicitStringFlag", + KEY_EXPLICIT_TAG: "keyExplicitTag", + KEY_EXPLICIT_BOOLEAN_TAG: "keyExplicitBooleanTag", + KEY_EXPLICIT_NUMBER_TAG: "keyExplicitNumberTag", + KEY_EXPLICIT_STRING_TAG: "keyExplicitStringTag", + KEY_EXPLICIT_ARRAY_TAG: "keyExplicitArrayTag", + KEY_ARRAY_INCLUDES: "keyArrayIncludes", + KEY_AGGREGATE: "keyAggregate", + KEY_AGGREGATE_ARGS: "keyAggregateArgs", + KEY_AGGREGATE_PARAMS: "keyAggregateParam", + L_PAREN: "lParen", + R_PAREN: "rParen", + VALUE_ISO_8601_DATE: "valueIso8601Date", + VALUE_RELATIVE_DATE: "valueRelativeDate", + VALUE_DURATION: "valueDuration", + VALUE_SIZE: "valueSize", + VALUE_PERCENTAGE: "valuePercentage", + VALUE_BOOLEAN: "valueBoolean", + VALUE_NUMBER: "valueNumber", + VALUE_TEXT: "valueText", + VALUE_NUMBER_LIST: "valueNumberList", + VALUE_TEXT_LIST: "valueTextList", +} as const; +export type Token = (typeof Token)[keyof typeof Token]; + +/** The operator in a `key:value` filter. Values match the frontend. */ +export const TermOperator = { + DEFAULT: "", + GREATER_THAN_EQUAL: ">=", + LESS_THAN_EQUAL: "<=", + GREATER_THAN: ">", + LESS_THAN: "<", + EQUAL: "=", + NOT_EQUAL: "!=", + // Internal wildcard operators; users write `*` instead. + CONTAINS: "\uF00DContains\uF00D", + DOES_NOT_CONTAIN: "\uF00DDoesNotContain\uF00D", + STARTS_WITH: "\uF00DStartsWith\uF00D", + DOES_NOT_START_WITH: "\uF00DDoesNotStartWith\uF00D", + ENDS_WITH: "\uF00DEndsWith\uF00D", + DOES_NOT_END_WITH: "\uF00DDoesNotEndWith\uF00D", + MATCHES: "//", + DOES_NOT_MATCH: "!//", +} as const; +export type TermOperator = (typeof TermOperator)[keyof typeof TermOperator]; + +export const BooleanOperator = { + AND: "AND", + OR: "OR", +} as const; +export type BooleanOperator = + (typeof BooleanOperator)[keyof typeof BooleanOperator]; + +/** Which kind of filter a {@link FilterToken} is. Values match the frontend. */ +export const FilterType = { + TEXT: "text", + TEXT_IN: "textIn", + DATE: "date", + SPECIFIC_DATE: "specificDate", + RELATIVE_DATE: "relativeDate", + DURATION: "duration", + SIZE: "size", + NUMERIC: "numeric", + NUMERIC_IN: "numericIn", + BOOLEAN: "boolean", + AGGREGATE_DURATION: "aggregateDuration", + AGGREGATE_SIZE: "aggregateSize", + AGGREGATE_PERCENTAGE: "aggregatePercentage", + AGGREGATE_NUMERIC: "aggregateNumeric", + AGGREGATE_DATE: "aggregateDate", + AGGREGATE_RELATIVE_DATE: "aggregateRelativeDate", + HAS: "has", + IS: "is", + ARRAY_INCLUDES: "arrayIncludes", +} as const; +export type FilterType = (typeof FilterType)[keyof typeof FilterType]; + +export type SourcePosition = { + offset: number; + line: number; + column: number; +}; + +export type SourceLocation = { + source: unknown; + start: SourcePosition; + end: SourcePosition; +}; + +type BaseToken = { + type: T; + /** The exact input text this token covers. */ + text: string; + location: SourceLocation; +}; + +/** + * Why a token is invalid. The structural converter never sets one; a + * subclass with product-specific validation can. + */ +export type InvalidToken = { + type: string; + reason: string; + expectedType?: FilterType[]; +} | null; + +export type SpacesToken = BaseToken & { value: string }; + +export type KeySimpleToken = BaseToken & { + value: string; + quoted: boolean; +}; + +type PrefixedKeyToken = BaseToken & { + prefix: string; + key: KeySimpleToken; +}; + +export type KeyExplicitFlagToken = PrefixedKeyToken< + typeof Token.KEY_EXPLICIT_FLAG +>; +export type KeyExplicitStringFlagToken = PrefixedKeyToken< + typeof Token.KEY_EXPLICIT_STRING_FLAG +>; +export type KeyExplicitNumberFlagToken = PrefixedKeyToken< + typeof Token.KEY_EXPLICIT_NUMBER_FLAG +>; +export type KeyExplicitTagToken = PrefixedKeyToken< + typeof Token.KEY_EXPLICIT_TAG +>; +export type KeyExplicitStringTagToken = PrefixedKeyToken< + typeof Token.KEY_EXPLICIT_STRING_TAG +>; +export type KeyExplicitNumberTagToken = PrefixedKeyToken< + typeof Token.KEY_EXPLICIT_NUMBER_TAG +>; +export type KeyExplicitBooleanTagToken = PrefixedKeyToken< + typeof Token.KEY_EXPLICIT_BOOLEAN_TAG +>; +export type KeyExplicitArrayTagToken = PrefixedKeyToken< + typeof Token.KEY_EXPLICIT_ARRAY_TAG +>; + +/** An array element key such as `foo[*]`. */ +export type KeyArrayIncludesToken = BaseToken< + typeof Token.KEY_ARRAY_INCLUDES +> & { + key: KeySimpleToken | KeyExplicitArrayTagToken; + index: string; +}; + +export type KeyAggregateParamToken = BaseToken< + typeof Token.KEY_AGGREGATE_PARAMS +> & { + value: string; + quoted: boolean; +}; + +export type ListItem = { separator: string; value: V | null }; + +export type KeyAggregateArgsToken = BaseToken< + typeof Token.KEY_AGGREGATE_ARGS +> & { + args: ListItem[]; +}; + +export type KeyAggregateToken = BaseToken & { + name: KeySimpleToken; + args: KeyAggregateArgsToken | null; + argsSpaceBefore: SpacesToken; + argsSpaceAfter: SpacesToken; +}; + +export type KeyToken = + | KeySimpleToken + | KeyExplicitFlagToken + | KeyExplicitStringFlagToken + | KeyExplicitNumberFlagToken + | KeyExplicitTagToken + | KeyExplicitStringTagToken + | KeyExplicitNumberTagToken + | KeyExplicitBooleanTagToken + | KeyExplicitArrayTagToken + | KeyArrayIncludesToken + | KeyAggregateToken; + +export type ValueIso8601DateToken = BaseToken< + typeof Token.VALUE_ISO_8601_DATE +> & { + value: string; + date: string; + time: string | undefined | null; + tz: string | undefined | null; +}; + +export type ValueRelativeDateToken = BaseToken< + typeof Token.VALUE_RELATIVE_DATE +> & { + value: string; + sign: "-" | "+"; + unit: "w" | "d" | "h" | "m"; +}; + +export type ValueDurationToken = BaseToken & { + value: string; + unit: string; +}; + +export type ValueSizeToken = BaseToken & { + value: string; + unit: string; +}; + +export type ValuePercentageToken = BaseToken & { + value: string; +}; + +export type ValueBooleanToken = BaseToken & { + value: string; +}; + +export type ValueNumberToken = BaseToken & { + value: string; + unit: string | null; +}; + +export type ValueTextToken = BaseToken & { + value: string; + quoted: boolean; +}; + +export type ValueNumberListToken = BaseToken & { + items: ListItem[]; +}; + +export type ValueTextListToken = BaseToken & { + items: ListItem[]; +}; + +export type ValueToken = + | ValueIso8601DateToken + | ValueRelativeDateToken + | ValueDurationToken + | ValueSizeToken + | ValuePercentageToken + | ValueBooleanToken + | ValueNumberToken + | ValueTextToken + | ValueNumberListToken + | ValueTextListToken; + +export type FilterToken = BaseToken & { + filter: FilterType; + key: KeyToken; + /** `has:` filters may hold a key token as their value. */ + value: ValueToken | KeyToken; + negated: boolean; + operator: TermOperator; + invalid: InvalidToken; +}; + +export type FreeTextToken = BaseToken & { + value: string; + quoted: boolean; + invalid: InvalidToken; +}; + +export type LogicBooleanToken = BaseToken & { + value: BooleanOperator; + invalid: InvalidToken; +}; + +export type LParenToken = BaseToken & { + value: "("; + invalid: InvalidToken; +}; + +export type RParenToken = BaseToken & { + value: ")"; + invalid: InvalidToken; +}; + +export type LogicGroupToken = BaseToken & { + inner: ParseResultToken[]; +}; + +/** A top-level token, or one directly inside a {@link LogicGroupToken}. */ +export type ParseResultToken = + | SpacesToken + | FilterToken + | FreeTextToken + | LogicBooleanToken + | LogicGroupToken + | LParenToken + | RParenToken; + +export type ParseResult = ParseResultToken[]; + +/** The structural part of the frontend's `SearchConfig`. */ +export type SearchConfig = { + /** Parse `key://pattern//` as a regex filter rather than literal text. */ + allowRegex: boolean; + booleanKeys: Set; + dateKeys: Set; + durationKeys: Set; + numericKeys: Set; + /** Aggregates whose filters take percentage values. */ + percentageKeys: Set; + sizeKeys: Set; + /** Text filter keys that may carry a comparison operator. */ + textOperatorKeys: Set; + /** Return paren tokens instead of {@link LogicGroupToken}s. */ + flattenParenGroups?: boolean; +}; + +/** The frontend's default key sets. */ +export const defaultConfig: SearchConfig = { + textOperatorKeys: new Set([ + "release.version", + "release.build", + "release.package", + "release.stage", + ]), + durationKeys: new Set(["transaction.duration"]), + percentageKeys: new Set(["percentage"]), + numericKeys: new Set([ + "project_id", + "project.id", + "issue.id", + "stack.colno", + "stack.lineno", + "stack.stack_level", + "transaction.duration", + ]), + dateKeys: new Set([ + "start", + "end", + "firstSeen", + "lastSeen", + "last_seen()", + "time", + "event.timestamp", + "timestamp", + "timestamp.to_hour", + "timestamp.to_day", + ]), + booleanKeys: new Set([ + "error.handled", + "error.unhandled", + "stack.in_app", + "team_key_transaction", + "symbolicated_in_app", + ]), + sizeKeys: new Set(), + allowRegex: false, +}; + +const MEASUREMENT_PATTERN = /^measurements\.[\w.-]+$/; +const SPAN_OP_BREAKDOWN_PREFIX = "spans."; + +/** + * Web and mobile vitals whose value type is a duration, from the frontend's + * `MEASUREMENT_FIELDS`. Other measurements are plain numbers. + */ +const DURATION_MEASUREMENTS = new Set([ + "measurements.fp", + "measurements.fcp", + "measurements.lcp", + "measurements.fid", + "measurements.ttfb", + "measurements.ttfb.requesttime", + "measurements.inp", + "measurements.app_start_cold", + "measurements.app_start_warm", + "measurements.stall_total_time", + "measurements.stall_longest_time", + "measurements.time_to_full_display", + "measurements.time_to_initial_display", +]); + +type GetKeyNameOptions = { + /** Include the arguments of aggregate keys, e.g. `p95(span.duration)`. */ + aggregateWithArgs?: boolean; +}; + +/** The key name the API expects for a filter key. Matches the frontend. */ +export function getKeyName( + key: KeyToken, + options: GetKeyNameOptions = {}, +): string { + switch (key.type) { + case Token.KEY_SIMPLE: + return key.value; + case Token.KEY_EXPLICIT_TAG: + return key.key.value; + case Token.KEY_AGGREGATE: + return options.aggregateWithArgs + ? `${key.name.value}(${key.args ? key.args.text : ""})` + : key.name.value; + case Token.KEY_ARRAY_INCLUDES: + return getKeyName(key.key); + default: + return key.text; + } +} + +type RawListItem = [ + space1: SpacesToken, + comma: string, + space2: SpacesToken, + value?: [notComma: undefined, value: V | null] | null, +]; + +function joinListItem([ + space1, + comma, + space2, + value, +]: RawListItem): ListItem { + return { + separator: [space1.value, comma, space2.value].join(""), + value: value ? value[1] : null, + }; +} + +type ConverterOptions = { + text: () => string; + location: () => SourceLocation; + config: C; +}; + +/** + * Builds tokens for the grammar. The grammar calls these methods directly, + * so their names and argument order follow the grammar actions. + */ +export class TokenConverter { + protected readonly text: () => string; + protected readonly location: () => SourceLocation; + protected readonly config: C; + + constructor({ text, location, config }: ConverterOptions) { + this.text = text; + this.location = location; + this.config = config; + } + + protected get defaultTokenFields(): { + text: string; + location: SourceLocation; + } { + return { text: this.text(), location: this.location() }; + } + + // -- Key type predicates ------------------------------------------------ + + isNumericKey(key: string): boolean { + return ( + this.config.numericKeys.has(key) || + MEASUREMENT_PATTERN.test(key) || + key.startsWith(SPAN_OP_BREAKDOWN_PREFIX) || + this.isDurationKey(key) || + this.isSizeKey(key) + ); + } + + isBooleanKey(key: string): boolean { + return this.config.booleanKeys.has(key); + } + + isPercentageKey(key: string): boolean { + return this.config.percentageKeys.has(key); + } + + isDateKey(key: string): boolean { + return this.config.dateKeys.has(key); + } + + isDurationKey(key: string): boolean { + return ( + this.config.durationKeys.has(key) || + key.startsWith(SPAN_OP_BREAKDOWN_PREFIX) || + DURATION_MEASUREMENTS.has(key) + ); + } + + isSizeKey(key: string): boolean { + return this.config.sizeKeys.has(key); + } + + // -- Validation hooks (structural parsing never marks tokens invalid) --- + + checkInvalidFilter( + _filter: FilterType, + _key: KeyToken, + _value: FilterToken["value"], + _negated: boolean, + _operator: TermOperator, + ): InvalidToken { + return null; + } + + checkInvalidFreeText(_value: string): InvalidToken { + return null; + } + + checkInvalidLogicalBoolean(_value: BooleanOperator): InvalidToken { + return null; + } + + checkInvalidParen(): InvalidToken { + return null; + } + + // -- Tokens ------------------------------------------------------------- + + tokenSpaces(value: string): SpacesToken { + return { ...this.defaultTokenFields, type: Token.SPACES, value }; + } + + tokenFilter( + filter: FilterType, + key: KeyToken, + value: FilterToken["value"], + operator: TermOperator | undefined | null, + negated: boolean, + wildcard?: TermOperator, + ): FilterToken { + let operatorToUse = operator ?? TermOperator.DEFAULT; + if ( + wildcard && + (filter === FilterType.TEXT || filter === FilterType.TEXT_IN) + ) { + operatorToUse = wildcard; + } + return { + ...this.defaultTokenFields, + type: Token.FILTER, + filter, + key, + value, + negated, + operator: operatorToUse, + invalid: this.checkInvalidFilter( + filter, + key, + value, + negated, + operatorToUse, + ), + }; + } + + tokenRegexFilter( + key: KeyToken, + literal: ValueTextToken, + pattern: ValueTextToken, + negated: boolean, + ): FilterToken { + const filter = + key.type === Token.KEY_ARRAY_INCLUDES + ? FilterType.ARRAY_INCLUDES + : FilterType.TEXT; + if (!this.config.allowRegex) { + return this.tokenFilter( + filter, + key, + literal, + TermOperator.DEFAULT, + negated, + ); + } + return this.tokenFilter( + filter, + key, + pattern, + TermOperator.MATCHES, + negated, + ); + } + + tokenLParen(value: "("): LParenToken { + return { + ...this.defaultTokenFields, + type: Token.L_PAREN, + value, + invalid: this.checkInvalidParen(), + }; + } + + tokenRParen(value: ")"): RParenToken { + return { + ...this.defaultTokenFields, + type: Token.R_PAREN, + value, + invalid: this.checkInvalidParen(), + }; + } + + tokenFreeText(value: string, quoted: boolean): FreeTextToken { + return { + ...this.defaultTokenFields, + type: Token.FREE_TEXT, + value, + quoted, + invalid: this.checkInvalidFreeText(value), + }; + } + + tokenLogicGroup(inner: ParseResultToken[]): LogicGroupToken { + return { ...this.defaultTokenFields, type: Token.LOGIC_GROUP, inner }; + } + + tokenLogicBoolean(value: BooleanOperator): LogicBooleanToken { + return { + ...this.defaultTokenFields, + type: Token.LOGIC_BOOLEAN, + value, + invalid: this.checkInvalidLogicalBoolean(value), + }; + } + + tokenKeySimple(value: string, quoted: boolean): KeySimpleToken { + return { + ...this.defaultTokenFields, + type: Token.KEY_SIMPLE, + value, + quoted, + }; + } + + tokenKeyExplicitFlag( + prefix: string, + key: KeySimpleToken, + ): KeyExplicitFlagToken { + return { + ...this.defaultTokenFields, + type: Token.KEY_EXPLICIT_FLAG, + prefix, + key, + }; + } + + tokenKeyExplicitStringFlag( + prefix: string, + key: KeySimpleToken, + ): KeyExplicitStringFlagToken { + return { + ...this.defaultTokenFields, + type: Token.KEY_EXPLICIT_STRING_FLAG, + prefix, + key, + }; + } + + tokenKeyExplicitNumberFlag( + prefix: string, + key: KeySimpleToken, + ): KeyExplicitNumberFlagToken { + return { + ...this.defaultTokenFields, + type: Token.KEY_EXPLICIT_NUMBER_FLAG, + prefix, + key, + }; + } + + tokenKeyExplicitTag( + prefix: string, + key: KeySimpleToken, + ): KeyExplicitTagToken { + return { + ...this.defaultTokenFields, + type: Token.KEY_EXPLICIT_TAG, + prefix, + key, + }; + } + + tokenKeyExplicitStringTag( + prefix: string, + key: KeySimpleToken, + ): KeyExplicitStringTagToken { + return { + ...this.defaultTokenFields, + type: Token.KEY_EXPLICIT_STRING_TAG, + prefix, + key, + }; + } + + tokenKeyExplicitNumberTag( + prefix: string, + key: KeySimpleToken, + ): KeyExplicitNumberTagToken { + return { + ...this.defaultTokenFields, + type: Token.KEY_EXPLICIT_NUMBER_TAG, + prefix, + key, + }; + } + + tokenKeyExplicitBooleanTag( + prefix: string, + key: KeySimpleToken, + ): KeyExplicitBooleanTagToken { + return { + ...this.defaultTokenFields, + type: Token.KEY_EXPLICIT_BOOLEAN_TAG, + prefix, + key, + }; + } + + tokenKeyExplicitArrayTag( + prefix: string, + key: KeySimpleToken, + ): KeyExplicitArrayTagToken { + return { + ...this.defaultTokenFields, + // The array-includes rule consumes `[*]` around this token, so build the + // text from the key to keep the tag identity operator-free. + text: `${prefix}[${key.text},array]`, + type: Token.KEY_EXPLICIT_ARRAY_TAG, + prefix, + key, + }; + } + + tokenKeyArrayIncludes( + key: KeySimpleToken | KeyExplicitArrayTagToken, + index: string, + ): KeyArrayIncludesToken { + return { + ...this.defaultTokenFields, + type: Token.KEY_ARRAY_INCLUDES, + key, + index, + }; + } + + tokenKeyAggregateParam( + value: string, + quoted: boolean, + ): KeyAggregateParamToken { + return { + ...this.defaultTokenFields, + type: Token.KEY_AGGREGATE_PARAMS, + value, + quoted, + }; + } + + tokenKeyAggregate( + name: KeySimpleToken, + args: KeyAggregateArgsToken | null, + argsSpaceBefore: SpacesToken, + argsSpaceAfter: SpacesToken, + ): KeyAggregateToken { + return { + ...this.defaultTokenFields, + type: Token.KEY_AGGREGATE, + name, + args, + argsSpaceBefore, + argsSpaceAfter, + }; + } + + tokenKeyAggregateArgs( + arg1: KeyAggregateParamToken, + args: RawListItem[], + ): KeyAggregateArgsToken { + return { + ...this.defaultTokenFields, + type: Token.KEY_AGGREGATE_ARGS, + args: [{ separator: "", value: arg1 }, ...args.map(joinListItem)], + }; + } + + tokenValueIso8601Date( + value: string, + date: (string | string[])[], + time?: (string | string[] | string[][])[] | null, + tz?: (string | string[])[] | string | null, + ): ValueIso8601DateToken { + return { + ...this.defaultTokenFields, + type: Token.VALUE_ISO_8601_DATE, + value, + date: date.flat().join(""), + time: Array.isArray(time) + ? time.flat().flat().join("").replace("T", "") + : time, + tz: Array.isArray(tz) ? tz.flat().join("") : tz, + }; + } + + tokenValueRelativeDate( + value: string, + sign: "-" | "+", + unit: "w" | "d" | "h" | "m", + ): ValueRelativeDateToken { + return { + ...this.defaultTokenFields, + type: Token.VALUE_RELATIVE_DATE, + value, + sign, + unit, + }; + } + + tokenValueDuration(value: string, unit: string): ValueDurationToken { + return { + ...this.defaultTokenFields, + type: Token.VALUE_DURATION, + value, + unit, + }; + } + + tokenValueSize(value: string, unit: string): ValueSizeToken { + return { ...this.defaultTokenFields, type: Token.VALUE_SIZE, value, unit }; + } + + tokenValuePercentage(value: string): ValuePercentageToken { + return { ...this.defaultTokenFields, type: Token.VALUE_PERCENTAGE, value }; + } + + tokenValueBoolean(value: string): ValueBooleanToken { + return { ...this.defaultTokenFields, type: Token.VALUE_BOOLEAN, value }; + } + + tokenValueNumber(value: string, unit: string | null): ValueNumberToken { + return { + ...this.defaultTokenFields, + type: Token.VALUE_NUMBER, + value, + unit, + }; + } + + tokenValueNumberList( + item1: ValueNumberToken, + items: RawListItem[], + ): ValueNumberListToken { + return { + ...this.defaultTokenFields, + type: Token.VALUE_NUMBER_LIST, + items: [{ separator: "", value: item1 }, ...items.map(joinListItem)], + }; + } + + tokenValueTextList( + item1: ValueTextToken, + items: RawListItem[], + ): ValueTextListToken { + return { + ...this.defaultTokenFields, + type: Token.VALUE_TEXT_LIST, + items: [{ separator: "", value: item1 }, ...items.map(joinListItem)], + }; + } + + tokenValueText(value: string, quoted: boolean): ValueTextToken { + return { + ...this.defaultTokenFields, + type: Token.VALUE_TEXT, + value, + quoted, + }; + } + + // -- Grammar predicates ------------------------------------------------- + + /** + * Whether a typed filter rule may match this key. Keys that fail every + * typed rule fall through to a text filter. + */ + predicateFilter(type: FilterType, key: KeyToken): boolean { + const keyName = getKeyName(key); + const checkAggregate = (check: (name: string) => boolean): boolean => + key.type === Token.KEY_AGGREGATE && + (key.args?.args.some((arg) => check(arg.value?.value ?? "")) ?? false); + + switch (type) { + case FilterType.NUMERIC: + case FilterType.NUMERIC_IN: + return this.isNumericKey(keyName); + case FilterType.DURATION: + return this.isDurationKey(keyName); + case FilterType.SIZE: + return this.isSizeKey(keyName); + case FilterType.BOOLEAN: + return this.isBooleanKey(keyName); + case FilterType.DATE: + case FilterType.RELATIVE_DATE: + case FilterType.SPECIFIC_DATE: + return this.isDateKey(keyName); + case FilterType.AGGREGATE_DURATION: + return checkAggregate((name) => this.isDurationKey(name)); + case FilterType.AGGREGATE_DATE: + return checkAggregate((name) => this.isDateKey(name)); + case FilterType.AGGREGATE_PERCENTAGE: + return checkAggregate((name) => this.isPercentageKey(name)); + default: + return true; + } + } + + /** Whether a text filter on this key may carry a comparison operator. */ + predicateTextOperator(key: KeyToken): boolean { + return this.config.textOperatorKeys.has(getKeyName(key)); + } + + /** Whether to build {@link LogicGroupToken}s for parenthesized terms. */ + predicateParenGroup(): boolean { + return !this.config.flattenParenGroups; + } +} + +export type TokenConverterClass = new ( + options: ConverterOptions, +) => TokenConverter; + +/** Thrown by {@link parseSearchOrThrow} when a query does not parse. */ +export type SearchSyntaxError = Error & { + found: string | null; + location: SourceLocation; +}; + +/** + * Apply `overrides` onto `base`, skipping `undefined` values so an unset + * override never clears a default. + */ +export function mergeSearchConfig( + base: C, + overrides: Partial, +): C { + return { + ...base, + ...Object.fromEntries( + Object.entries(overrides).filter(([, value]) => value !== undefined), + ), + }; +} + +type ParseSearchOptions = { + /** Overrides applied onto {@link defaultConfig}. */ + config?: Partial; + /** A converter subclass, e.g. one that adds validation. */ + converter?: TokenConverterClass; +}; + +/** + * Parse a Sentry search query. Throws a {@link SearchSyntaxError} when the + * query does not match the grammar. + */ +export function parseSearchOrThrow( + query: string, + options: ParseSearchOptions = {}, +): ParseResult { + const config = options.config + ? mergeSearchConfig(defaultConfig as C, options.config) + : defaultConfig; + return parseGrammar(query, { + config, + TokenConverter: options.converter ?? TokenConverter, + TermOperator, + FilterType, + }) as ParseResult; +} + +/** + * Parse a Sentry search query, or return `null` when it does not match the + * grammar. Mirrors the frontend's `parseSearch`. + */ +export function parseSearch( + query: string, + options: ParseSearchOptions = {}, +): ParseResult | null { + try { + return parseSearchOrThrow(query, options); + } catch { + return null; + } +} diff --git a/packages/toolkit-core/test/fixtures/search-syntax/aggregate_duration_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/aggregate_duration_filter.json new file mode 100644 index 000000000..168735e0d --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/aggregate_duration_filter.json @@ -0,0 +1,80 @@ +[ + { + "query": "avg(transaction.duration):>500s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateDuration", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "avg", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "transaction.duration", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": ">", + "value": { + "type": "valueDuration", + "value": "500", + "unit": "s", + "parsed": {"value": 500000} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "TypeError avg(transaction.duration):>500s", + "result": [ + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "TypeError ", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateDuration", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "avg", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "transaction.duration", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": ">", + "value": { + "type": "valueDuration", + "value": "500", + "unit": "s", + "parsed": {"value": 500000} + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/aggregate_duration_filter_overrides_numeric_shorthand.json b/packages/toolkit-core/test/fixtures/search-syntax/aggregate_duration_filter_overrides_numeric_shorthand.json new file mode 100644 index 000000000..4307cefe2 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/aggregate_duration_filter_overrides_numeric_shorthand.json @@ -0,0 +1,41 @@ +[ + { + "desc": "2m should mean 2 minutes for duration filters (as opposed to 2 million)", + "query": "avg(transaction.duration):>2m", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateDuration", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "avg", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "transaction.duration", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": ">", + "value": { + "type": "valueDuration", + "value": "2", + "unit": "m", + "parsed": {"value": 120000} + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/aggregate_filter_on_explicit_typed_attributes.json b/packages/toolkit-core/test/fixtures/search-syntax/aggregate_filter_on_explicit_typed_attributes.json new file mode 100644 index 000000000..bcc560bd2 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/aggregate_filter_on_explicit_typed_attributes.json @@ -0,0 +1,192 @@ +[ + { + "query": "count_unique(tags[foo]):>500", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "count_unique", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "tags[foo]", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": ">", + "value": { + "type": "valueNumber", + "value": "500", + "unit": null, + "parsed": {"value": 500} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "avg(tags[foo,number]):>500", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "avg", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "tags[foo,number]", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": ">", + "value": { + "type": "valueNumber", + "value": "500", + "unit": null, + "parsed": {"value": 500} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "avg(tags[foo:bar,number]):>500", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "avg", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "tags[foo:bar,number]", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": ">", + "value": { + "type": "valueNumber", + "value": "500", + "unit": null, + "parsed": {"value": 500} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "count_unique(tags[foo,string]):>500", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "count_unique", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "tags[foo,string]", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": ">", + "value": { + "type": "valueNumber", + "value": "500", + "unit": null, + "parsed": {"value": 500} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "count_unique(tags[foo:bar,string]):>500", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "count_unique", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "tags[foo:bar,string]", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": ">", + "value": { + "type": "valueNumber", + "value": "500", + "unit": null, + "parsed": {"value": 500} + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/aggregate_rel_time_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/aggregate_rel_time_filter.json new file mode 100644 index 000000000..7316cfdf7 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/aggregate_rel_time_filter.json @@ -0,0 +1,79 @@ +[ + { + "query": "last_seen():+7d", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateRelativeDate", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "last_seen", "quoted": false}, + "args": null, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": "", + "value": { + "type": "valueRelativeDate", + "value": "7", + "sign": "+", + "unit": "d", + "parsed": { + "value": "2017-10-24T02:41:20.000Z" + } + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "last_seen():-2w", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateRelativeDate", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "last_seen", "quoted": false}, + "args": null, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": "", + "value": { + "type": "valueRelativeDate", + "value": "2", + "sign": "-", + "unit": "w", + "parsed": { + "value": "2017-10-03T02:41:20.000Z" + } + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "random:-2w", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "random", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "-2w", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/basic_fallthrough.json b/packages/toolkit-core/test/fixtures/search-syntax/basic_fallthrough.json new file mode 100644 index 000000000..fddb456c6 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/basic_fallthrough.json @@ -0,0 +1,135 @@ +[ + { + "query": "random:2018-01-01", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "random", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": ">2018-01-01", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "random:hello", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "random", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "hello", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "random:123", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "random", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "123", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/boolean_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/boolean_filter.json new file mode 100644 index 000000000..2350733a8 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/boolean_filter.json @@ -0,0 +1,107 @@ +[ + { + "query": "stack.in_app:true", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "boolean", + "negated": false, + "key": {"type": "keySimple", "value": "stack.in_app", "quoted": false}, + "operator": "", + "value": {"type": "valueBoolean", "value": "true", "parsed": {"value": true}} + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "stack.in_app:TRUE", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "boolean", + "negated": false, + "key": {"type": "keySimple", "value": "stack.in_app", "quoted": false}, + "operator": "", + "value": {"type": "valueBoolean", "value": "TRUE", "parsed": {"value": true}} + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "stack.in_app:1", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "boolean", + "negated": false, + "key": {"type": "keySimple", "value": "stack.in_app", "quoted": false}, + "operator": "", + "value": {"type": "valueBoolean", "value": "1", "parsed": {"value": true}} + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "stack.in_app:false", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "boolean", + "negated": false, + "key": {"type": "keySimple", "value": "stack.in_app", "quoted": false}, + "operator": "", + "value": {"type": "valueBoolean", "value": "false", "parsed": {"value": false}} + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "stack.in_app:FALSE", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "boolean", + "negated": false, + "key": {"type": "keySimple", "value": "stack.in_app", "quoted": false}, + "operator": "", + "value": {"type": "valueBoolean", "value": "FALSE", "parsed": {"value": false}} + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "stack.in_app:0", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "boolean", + "negated": false, + "key": {"type": "keySimple", "value": "stack.in_app", "quoted": false}, + "operator": "", + "value": {"type": "valueBoolean", "value": "0", "parsed": {"value": false}} + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!stack.in_app:false", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "boolean", + "negated": true, + "key": {"type": "keySimple", "value": "stack.in_app", "quoted": false}, + "operator": "", + "value": {"type": "valueBoolean", "value": "false", "parsed": {"value": false}} + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/boolean_filter_passthrough.json b/packages/toolkit-core/test/fixtures/search-syntax/boolean_filter_passthrough.json new file mode 100644 index 000000000..6112685e6 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/boolean_filter_passthrough.json @@ -0,0 +1,22 @@ +[ + { + "query": "project_id:1", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numeric", + "negated": false, + "key": {"type": "keySimple", "value": "project_id", "quoted": false}, + "operator": "", + "value": { + "type": "valueNumber", + "value": "1", + "unit": null, + "parsed": {"value": 1} + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/contains_operator.json b/packages/toolkit-core/test/fixtures/search-syntax/contains_operator.json new file mode 100644 index 000000000..0e8efa5fa --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/contains_operator.json @@ -0,0 +1,86 @@ +[ + { + "query": "browser.name:Containsfirefox", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "operator": "Contains", + "key": {"type": "keySimple", "value": "browser.name", "quoted": false}, + "value": {"type": "valueText", "value": "firefox", "quoted": false} + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "browser.name:Contains[firefox, chrome]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "operator": "Contains", + "key": {"type": "keySimple", "value": "browser.name", "quoted": false}, + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": {"type": "valueText", "value": "firefox", "quoted": false} + }, + { + "separator": ", ", + "value": {"type": "valueText", "value": "chrome", "quoted": false} + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!browser.name:Containsfirefox", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": true, + "operator": "Contains", + "key": {"type": "keySimple", "value": "browser.name", "quoted": false}, + "value": {"type": "valueText", "value": "firefox", "quoted": false} + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!browser.name:Contains[firefox, chrome]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": true, + "operator": "Contains", + "key": {"type": "keySimple", "value": "browser.name", "quoted": false}, + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": {"type": "valueText", "value": "firefox", "quoted": false} + }, + { + "separator": ", ", + "value": {"type": "valueText", "value": "chrome", "quoted": false} + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/custom_explicit_tag.json b/packages/toolkit-core/test/fixtures/search-syntax/custom_explicit_tag.json new file mode 100644 index 000000000..d2198878e --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/custom_explicit_tag.json @@ -0,0 +1,55 @@ +[ + { + "query": "tags[fruit]:apple release:1.2.1 tags[project_id]:123", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": { + "type": "keyExplicitTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "fruit", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueText", + "value": "apple", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "1.2.1", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": { + "type": "keyExplicitTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "project_id", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueText", + "value": "123", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/custom_tag.json b/packages/toolkit-core/test/fixtures/search-syntax/custom_tag.json new file mode 100644 index 000000000..744aaf32d --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/custom_tag.json @@ -0,0 +1,72 @@ +[ + { + "query": "fruit:apple release:1.2.1", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "fruit", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "apple", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "1.2.1", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "p95:>5s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "p95", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": ">5s", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "p95:>5k", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "p95", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": ">5k", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/disallow_wildcard_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/disallow_wildcard_filter.json new file mode 100644 index 000000000..e1ad356d4 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/disallow_wildcard_filter.json @@ -0,0 +1,194 @@ +[ + { + "desc": "FreeText with wildcard shall be invalid if disallowWildcard is true", + "query": "freeTextWith*", + "additionalConfig": { + "disallowWildcard": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "freeText", + "value": "freeTextWith*", + "quoted": false, + "invalid": { + "type": "wildcard-not-allowed", + "reason": "Wildcards not supported in search" + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "List value with wildcard shall be invalid if disallowWildcard is true", + "query": "release:[*]", + "additionalConfig": { + "disallowWildcard": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "textIn", + "invalid": { + "type": "wildcard-not-allowed", + "reason": "Wildcards not supported in search" + }, + "key": { + "type": "keySimple", + "value": "release", + "quoted": false + }, + "operator": "", + "negated": false, + "value": { + "items": [ + { + "separator": "", + "value": { + "quoted": false, + "type": "valueText", + "value": "*" + } + } + ], + "type": "valueTextList" + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "Value shall be invalid if disallowWildcard is true", + "query": "release:*", + "additionalConfig": { + "disallowWildcard": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": { + "type": "keySimple", + "value": "release", + "quoted": false + }, + "operator": "", + "value": { + "type": "valueText", + "value": "*", + "quoted": false + }, + "invalid": { + "type": "wildcard-not-allowed", + "reason": "Wildcards not supported in search" + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "Key shall be invalid if not in supportedTags", + "query": "release*:1", + "additionalConfig": { + "disallowWildcard": true, + "supportedTags": { + "release": { + "key": "release", + "kind": "field" + } + } + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "freeText", + "value": "release*:1", + "quoted": false, + "invalid": { + "type": "wildcard-not-allowed", + "reason": "Wildcards not supported in search" + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "If disallowWildcard is true and a custom reason is provided, it shall be shown", + "query": "rele*ase", + "additionalConfig": { + "disallowWildcard": true, + "invalidMessages": { + "wildcard-not-allowed": "Release queries don't support wildcards" + } + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "freeText", + "value": "rele*ase", + "quoted": false, + "invalid": { + "type": "wildcard-not-allowed", + "reason": "Release queries don't support wildcards" + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "If disallowWildcard is true and a custom reason is provided, it shall be shown", + "query": "rele*ase", + "additionalConfig": { + "disallowWildcard": true, + "invalidMessages": { + "wildcard-not-allowed": "Release queries don't support wildcards" + } + }, + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "freeText", + "value": "rele*ase", + "quoted": false, + "invalid": { + "type": "wildcard-not-allowed", + "reason": "Release queries don't support wildcards" + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/duration_aggregate_measurements_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/duration_aggregate_measurements_filter.json new file mode 100644 index 000000000..b2d72aa65 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/duration_aggregate_measurements_filter.json @@ -0,0 +1,128 @@ +[ + { + "query": "percentile(measurements.fp, 0.5):3.3s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateDuration", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "percentile", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "measurements.fp", + "quoted": false + } + }, + { + "separator": ", ", + "value": {"type": "keyAggregateParam", "value": "0.5", "quoted": false} + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": "", + "value": { + "type": "valueDuration", + "value": "3.3", + "unit": "s", + "parsed": {"value": 3300} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "percentile(measurements.fp, 0.5):>3.3s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateDuration", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "percentile", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "measurements.fp", + "quoted": false + } + }, + { + "separator": ", ", + "value": {"type": "keyAggregateParam", "value": "0.5", "quoted": false} + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": ">", + "value": { + "type": "valueDuration", + "value": "3.3", + "unit": "s", + "parsed": {"value": 3300} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "percentile(measurements.fp, 0.5):<3.3s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateDuration", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "percentile", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "measurements.fp", + "quoted": false + } + }, + { + "separator": ", ", + "value": {"type": "keyAggregateParam", "value": "0.5", "quoted": false} + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": "<", + "value": { + "type": "valueDuration", + "value": "3.3", + "unit": "s", + "parsed": {"value": 3300} + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/duration_aggregate_op_breakdowns_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/duration_aggregate_op_breakdowns_filter.json new file mode 100644 index 000000000..cee58f03b --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/duration_aggregate_op_breakdowns_filter.json @@ -0,0 +1,128 @@ +[ + { + "query": "percentile(spans.browser, 0.5):3.3s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateDuration", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "percentile", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "spans.browser", + "quoted": false + } + }, + { + "separator": ", ", + "value": {"type": "keyAggregateParam", "value": "0.5", "quoted": false} + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": "", + "value": { + "type": "valueDuration", + "value": "3.3", + "unit": "s", + "parsed": {"value": 3300} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "percentile(spans.browser, 0.5):>3.3s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateDuration", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "percentile", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "spans.browser", + "quoted": false + } + }, + { + "separator": ", ", + "value": {"type": "keyAggregateParam", "value": "0.5", "quoted": false} + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": ">", + "value": { + "type": "valueDuration", + "value": "3.3", + "unit": "s", + "parsed": {"value": 3300} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "percentile(spans.browser, 0.5):<3.3s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateDuration", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "percentile", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "spans.browser", + "quoted": false + } + }, + { + "separator": ", ", + "value": {"type": "keyAggregateParam", "value": "0.5", "quoted": false} + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": "<", + "value": { + "type": "valueDuration", + "value": "3.3", + "unit": "s", + "parsed": {"value": 3300} + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/duration_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/duration_filter.json new file mode 100644 index 000000000..e2ce7d779 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/duration_filter.json @@ -0,0 +1,42 @@ +[ + { + "query": "transaction.duration:>500s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "duration", + "negated": false, + "key": {"type": "keySimple", "value": "transaction.duration", "quoted": false}, + "operator": ">", + "value": { + "type": "valueDuration", + "value": "500", + "unit": "s", + "parsed": {"value": 500000} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!transaction.duration:500s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "duration", + "negated": true, + "key": {"type": "keySimple", "value": "transaction.duration", "quoted": false}, + "operator": "", + "value": { + "type": "valueDuration", + "value": "500", + "unit": "s", + "parsed": {"value": 500000} + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/duration_filter_overrides_numeric_shorthand.json b/packages/toolkit-core/test/fixtures/search-syntax/duration_filter_overrides_numeric_shorthand.json new file mode 100644 index 000000000..a81ba0d9c --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/duration_filter_overrides_numeric_shorthand.json @@ -0,0 +1,23 @@ +[ + { + "desc": "2m should mean 2 minutes for duration filters (as opposed to 2 million)", + "query": "transaction.duration:>2m", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "duration", + "negated": false, + "key": {"type": "keySimple", "value": "transaction.duration", "quoted": false}, + "operator": ">", + "value": { + "type": "valueDuration", + "value": "2", + "unit": "m", + "parsed": {"value": 120000} + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/duration_measurements_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/duration_measurements_filter.json new file mode 100644 index 000000000..56f7bf875 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/duration_measurements_filter.json @@ -0,0 +1,62 @@ +[ + { + "query": "measurements.fp:1.5s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "duration", + "negated": false, + "key": {"type": "keySimple", "value": "measurements.fp", "quoted": false}, + "operator": "", + "value": { + "type": "valueDuration", + "value": "1.5", + "unit": "s", + "parsed": {"value": 1500} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "measurements.fp:>1.5s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "duration", + "negated": false, + "key": {"type": "keySimple", "value": "measurements.fp", "quoted": false}, + "operator": ">", + "value": { + "type": "valueDuration", + "value": "1.5", + "unit": "s", + "parsed": {"value": 1500} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "measurements.fp:<1.5s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "duration", + "negated": false, + "key": {"type": "keySimple", "value": "measurements.fp", "quoted": false}, + "operator": "<", + "value": { + "type": "valueDuration", + "value": "1.5", + "unit": "s", + "parsed": {"value": 1500} + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/duration_on_non_duration_field.json b/packages/toolkit-core/test/fixtures/search-syntax/duration_on_non_duration_field.json new file mode 100644 index 000000000..76d03df7a --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/duration_on_non_duration_field.json @@ -0,0 +1,21 @@ +[ + { + "query": "user.id:500s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "user.id", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "500s", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/duration_op_breakdowns_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/duration_op_breakdowns_filter.json new file mode 100644 index 000000000..87b8f9811 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/duration_op_breakdowns_filter.json @@ -0,0 +1,62 @@ +[ + { + "query": "spans.browser:1.5s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "duration", + "negated": false, + "key": {"type": "keySimple", "value": "spans.browser", "quoted": false}, + "operator": "", + "value": { + "type": "valueDuration", + "value": "1.5", + "unit": "s", + "parsed": {"value": 1500} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "spans.browser:>1.5s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "duration", + "negated": false, + "key": {"type": "keySimple", "value": "spans.browser", "quoted": false}, + "operator": ">", + "value": { + "type": "valueDuration", + "value": "1.5", + "unit": "s", + "parsed": {"value": 1500} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "spans.browser:<1.5s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "duration", + "negated": false, + "key": {"type": "keySimple", "value": "spans.browser", "quoted": false}, + "operator": "<", + "value": { + "type": "valueDuration", + "value": "1.5", + "unit": "s", + "parsed": {"value": 1500} + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/empty_filter_value.json b/packages/toolkit-core/test/fixtures/search-syntax/empty_filter_value.json new file mode 100644 index 000000000..f214021b2 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/empty_filter_value.json @@ -0,0 +1,44 @@ +[ + { + "query": "device.family:\"\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "device.family", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "", + "quoted": true + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "device.family:", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "filter-must-have-value", + "reason": "Filter must have a value" + }, + "key": {"type": "keySimple", "value": "device.family", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/empty_spaces_stripped_correctly.json b/packages/toolkit-core/test/fixtures/search-syntax/empty_spaces_stripped_correctly.json new file mode 100644 index 000000000..754194830 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/empty_spaces_stripped_correctly.json @@ -0,0 +1,34 @@ +[ + { + "query": "event.type:transaction transaction:/organizations/:orgId/discover/results/", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "event.type", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "transaction", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "transaction", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "/organizations/:orgId/discover/results/", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/empty_string.json b/packages/toolkit-core/test/fixtures/search-syntax/empty_string.json new file mode 100644 index 000000000..fd48840ec --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/empty_string.json @@ -0,0 +1,7 @@ +[ + { + "desc": "Empty quotations become a dropped term", + "query": "", + "result": [{"type": "spaces", "value": ""}] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/ends_with_operator.json b/packages/toolkit-core/test/fixtures/search-syntax/ends_with_operator.json new file mode 100644 index 000000000..2cbcad530 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/ends_with_operator.json @@ -0,0 +1,86 @@ +[ + { + "query": "browser.name:EndsWithfirefox", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "operator": "EndsWith", + "key": {"type": "keySimple", "value": "browser.name", "quoted": false}, + "value": {"type": "valueText", "value": "firefox", "quoted": false} + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "browser.name:EndsWith[firefox, chrome]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "operator": "EndsWith", + "key": {"type": "keySimple", "value": "browser.name", "quoted": false}, + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": {"type": "valueText", "value": "firefox", "quoted": false} + }, + { + "separator": ", ", + "value": {"type": "valueText", "value": "chrome", "quoted": false} + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!browser.name:EndsWithfirefox", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": true, + "operator": "EndsWith", + "key": {"type": "keySimple", "value": "browser.name", "quoted": false}, + "value": {"type": "valueText", "value": "firefox", "quoted": false} + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!browser.name:EndsWith[firefox, chrome]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": true, + "operator": "EndsWith", + "key": {"type": "keySimple", "value": "browser.name", "quoted": false}, + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": {"type": "valueText", "value": "firefox", "quoted": false} + }, + { + "separator": ", ", + "value": {"type": "valueText", "value": "chrome", "quoted": false} + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/escaped_quote_value.json b/packages/toolkit-core/test/fixtures/search-syntax/escaped_quote_value.json new file mode 100644 index 000000000..3fa9d057c --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/escaped_quote_value.json @@ -0,0 +1,62 @@ +[ + { + "query": "device.family:\\\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "device.family", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "\\\"", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "device.family:te\\\"st", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "device.family", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "te\\\"st", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "This is a weird case. I think this should be an error, but it doesn't seem trivial to rewrite the grammar to handle that.", + "query": "url:\"te\"st", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "url", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "te", + "quoted": true + } + }, + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "st", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/escaped_quotes.json b/packages/toolkit-core/test/fixtures/search-syntax/escaped_quotes.json new file mode 100644 index 000000000..54b88671c --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/escaped_quotes.json @@ -0,0 +1,40 @@ +[ + { + "query": "release:\"a\\\"thing\\\"\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "a\\\"thing\\\"", + "quoted": true + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "release:\"a\\\"\\\"release\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "a\\\"\\\"release", + "quoted": true + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/explicit_array_tag.json b/packages/toolkit-core/test/fixtures/search-syntax/explicit_array_tag.json new file mode 100644 index 000000000..00d9ee7e2 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/explicit_array_tag.json @@ -0,0 +1,87 @@ +[ + { + "query": "tags[fruit[*],array]:apple release:1.2.1 tags[project_id[*],array]:123", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "arrayIncludes", + "key": { + "type": "keyArrayIncludes", + "key": { + "type": "keyExplicitArrayTag", + "prefix": "tags", + "key": { + "type": "keySimple", + "value": "fruit", + "quoted": false + } + }, + "index": "*" + }, + "value": { + "type": "valueText", + "value": "apple", + "quoted": false + }, + "negated": false, + "operator": "" + }, + { + "type": "spaces", + "value": " " + }, + { + "type": "filter", + "filter": "text", + "key": { + "type": "keySimple", + "value": "release", + "quoted": false + }, + "value": { + "type": "valueText", + "value": "1.2.1", + "quoted": false + }, + "negated": false, + "operator": "" + }, + { + "type": "spaces", + "value": " " + }, + { + "type": "filter", + "filter": "arrayIncludes", + "key": { + "type": "keyArrayIncludes", + "key": { + "type": "keyExplicitArrayTag", + "prefix": "tags", + "key": { + "type": "keySimple", + "value": "project_id", + "quoted": false + } + }, + "index": "*" + }, + "value": { + "type": "valueText", + "value": "123", + "quoted": false + }, + "negated": false, + "operator": "" + }, + { + "type": "spaces", + "value": "" + } + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/explicit_boolean_tag.json b/packages/toolkit-core/test/fixtures/search-syntax/explicit_boolean_tag.json new file mode 100644 index 000000000..63bfe309e --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/explicit_boolean_tag.json @@ -0,0 +1,55 @@ +[ + { + "query": "tags[foo,boolean]:true release:1.2.1 tags[project_id,boolean]:false", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": { + "type": "keyExplicitBooleanTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "foo", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueText", + "value": "true", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "1.2.1", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": { + "type": "keyExplicitBooleanTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "project_id", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueText", + "value": "false", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/explicit_number_tag.json b/packages/toolkit-core/test/fixtures/search-syntax/explicit_number_tag.json new file mode 100644 index 000000000..9ee432a6c --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/explicit_number_tag.json @@ -0,0 +1,55 @@ +[ + { + "query": "tags[foo,number]:456 release:1.2.1 tags[project_id,number]:123", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": { + "type": "keyExplicitNumberTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "foo", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueText", + "value": "456", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "1.2.1", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": { + "type": "keyExplicitNumberTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "project_id", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueText", + "value": "123", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/explicit_number_tags_in_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/explicit_number_tags_in_filter.json new file mode 100644 index 000000000..681e1d38b --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/explicit_number_tags_in_filter.json @@ -0,0 +1,41 @@ +[ + { + "query": "tags[foo,number]:[123, 456]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": { + "type": "keyExplicitNumberTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "foo", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "123", + "quoted": false + } + }, + { + "separator": ", ", + "value": { + "type": "valueText", + "value": "456", + "quoted": false + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/explicit_string_tag.json b/packages/toolkit-core/test/fixtures/search-syntax/explicit_string_tag.json new file mode 100644 index 000000000..95bf5320c --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/explicit_string_tag.json @@ -0,0 +1,55 @@ +[ + { + "query": "tags[fruit,string]:apple release:1.2.1 tags[project_id,string]:123", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": { + "type": "keyExplicitStringTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "fruit", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueText", + "value": "apple", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "1.2.1", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": { + "type": "keyExplicitStringTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "project_id", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueText", + "value": "123", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/explicit_string_tags_in_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/explicit_string_tags_in_filter.json new file mode 100644 index 000000000..16498eb1a --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/explicit_string_tags_in_filter.json @@ -0,0 +1,80 @@ +[ + { + "query": "tags[fruit,string]:[apple, pear]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": { + "type": "keyExplicitStringTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "fruit", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "apple", + "quoted": false + } + }, + { + "separator": ", ", + "value": { + "type": "valueText", + "value": "pear", + "quoted": false + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "tags[fruit,string]:[\"apple wow\", \"pear\"]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": { + "type": "keyExplicitStringTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "fruit", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "apple wow", + "quoted": true + } + }, + { + "separator": ", ", + "value": { + "type": "valueText", + "value": "pear", + "quoted": true + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/explicit_tags_in_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/explicit_tags_in_filter.json new file mode 100644 index 000000000..c85b99915 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/explicit_tags_in_filter.json @@ -0,0 +1,80 @@ +[ + { + "query": "tags[fruit]:[apple, pear]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": { + "type": "keyExplicitTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "fruit", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "apple", + "quoted": false + } + }, + { + "separator": ", ", + "value": { + "type": "valueText", + "value": "pear", + "quoted": false + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "tags[fruit]:[\"apple wow\", \"pear\"]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": { + "type": "keyExplicitTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "fruit", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "apple wow", + "quoted": true + } + }, + { + "separator": ", ", + "value": { + "type": "valueText", + "value": "pear", + "quoted": true + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/free_text_search_anywhere.json b/packages/toolkit-core/test/fixtures/search-syntax/free_text_search_anywhere.json new file mode 100644 index 000000000..ecf438d2a --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/free_text_search_anywhere.json @@ -0,0 +1,85 @@ +[ + { + "query": "hello what user.email:foo@example.com where release:1.2.1 when", + "result": [ + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "hello what ", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "user.email", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "foo@example.com", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + {"type": "freeText", "value": "where ", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "1.2.1", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + {"type": "freeText", "value": "when", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "hello", + "result": [ + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "hello", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""} + ] + }, + { + "query": " hello ", + "result": [ + {"type": "spaces", "value": " "}, + {"type": "freeText", "value": "hello ", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""} + ] + }, + { + "query": " hello there", + "result": [ + {"type": "spaces", "value": " "}, + {"type": "freeText", "value": "hello there", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""} + ] + }, + { + "query": " hello there:bye", + "result": [ + {"type": "spaces", "value": " "}, + {"type": "freeText", "value": "hello ", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "there", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "bye", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/has_tag.json b/packages/toolkit-core/test/fixtures/search-syntax/has_tag.json new file mode 100644 index 000000000..0d8ce7ba9 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/has_tag.json @@ -0,0 +1,62 @@ +[ + { + "desc": "unquoted key", + "query": "has:release", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "has", + "negated": false, + "key": {"type": "keySimple", "value": "has", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "release", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "quoted key", + "query": "has:\"hi:there\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "has", + "negated": false, + "key": {"type": "keySimple", "value": "has", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "hi:there", + "quoted": true + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "malformed key", + "query": "has:\"hi there\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "has", + "negated": false, + "key": {"type": "keySimple", "value": "has", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "hi there", + "quoted": true + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_column_with_duration_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_column_with_duration_filter.json new file mode 100644 index 000000000..a105d967f --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_column_with_duration_filter.json @@ -0,0 +1,15 @@ +[ + { + "query": "avg(stack.colno):>500s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "freeText", + "quoted": false, + "value": "avg(stack.colno):>500s", + "invalid": null + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_duration_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_duration_filter.json new file mode 100644 index 000000000..b777144d1 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_duration_filter.json @@ -0,0 +1,15 @@ +[ + { + "query": "avg(transaction.duration):>..500s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "freeText", + "value": "avg(transaction.duration):>..500s", + "quoted": false, + "invalid": null + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_empty_parameter.json b/packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_empty_parameter.json new file mode 100644 index 000000000..dec8d006e --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_empty_parameter.json @@ -0,0 +1,48 @@ +[ + { + "query": "percentile(measurements.fp,):3.3s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateDuration", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "percentile", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "measurements.fp", + "quoted": false + } + }, + { + "separator": ",", + "value": null + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": "", + "value": { + "type": "valueDuration", + "value": "3.3", + "unit": "s", + "parsed": {"value": 3300} + }, + "invalid": { + "reason": "Function parameters should not have empty values", + "type": "empty-parameter-not-allowed" + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_percentage_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_percentage_filter.json new file mode 100644 index 000000000..4395c14da --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/invalid_aggregate_percentage_filter.json @@ -0,0 +1,15 @@ +[ + { + "query": "percentage(transaction.duration, transaction.duration):>..500%", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "freeText", + "value": "percentage(transaction.duration, transaction.duration):>..500%", + "quoted": false, + "invalid": null + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/invalid_boolean_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/invalid_boolean_filter.json new file mode 100644 index 000000000..38bab1cf8 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/invalid_boolean_filter.json @@ -0,0 +1,74 @@ +[ + { + "query": "stack.in_app:lol", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "invalid-boolean", + "reason": "Invalid boolean. Expected true, 1, false, or 0.", + "expectedType": ["boolean"] + }, + "key": {"type": "keySimple", "value": "stack.in_app", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "lol", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "stack.in_app:123", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "invalid-boolean", + "reason": "Invalid boolean. Expected true, 1, false, or 0.", + "expectedType": ["boolean"] + }, + "key": {"type": "keySimple", "value": "stack.in_app", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "123", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "stack.in_app:>true", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "invalid-boolean", + "reason": "Invalid boolean. Expected true, 1, false, or 0.", + "expectedType": ["boolean"] + }, + "key": {"type": "keySimple", "value": "stack.in_app", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": ">true", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/invalid_date_formats.json b/packages/toolkit-core/test/fixtures/search-syntax/invalid_date_formats.json new file mode 100644 index 000000000..a65a6f4f7 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/invalid_date_formats.json @@ -0,0 +1,74 @@ +[ + { + "query": "timestamp:hello", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "invalid-date-format", + "reason": "Invalid date format. Expected +/-duration (e.g. +1h) or ISO 8601-like (e.g. 2017-10-17 or 2017-10-17T02:41:00.000Z)", + "expectedType": ["date", "specificDate", "relativeDate"] + }, + "key": {"type": "keySimple", "value": "timestamp", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "hello", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "timestamp:123", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "invalid-date-format", + "reason": "Invalid date format. Expected +/-duration (e.g. +1h) or ISO 8601-like (e.g. 2017-10-17 or 2017-10-17T02:41:00.000Z)", + "expectedType": ["date", "specificDate", "relativeDate"] + }, + "key": {"type": "keySimple", "value": "timestamp", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "123", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "timestamp:2018-01-01T00:01ZZ", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "invalid-date-format", + "reason": "Invalid date format. Expected +/-duration (e.g. +1h) or ISO 8601-like (e.g. 2017-10-17 or 2017-10-17T02:41:00.000Z)", + "expectedType": ["date", "specificDate", "relativeDate"] + }, + "key": {"type": "keySimple", "value": "timestamp", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "2018-01-01T00:01ZZ", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/invalid_duration_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/invalid_duration_filter.json new file mode 100644 index 000000000..fa00d622d --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/invalid_duration_filter.json @@ -0,0 +1,26 @@ +[ + { + "query": "transaction.duration:>..500s", + "result": [ + {"type": "spaces", "value": ""}, + { + "filter": "text", + "negated": false, + "operator": "", + "invalid": { + "type": "invalid-duration", + "reason": "Invalid duration. Expected number followed by duration unit suffix", + "expectedType": ["duration"] + }, + "key": {"quoted": false, "type": "keySimple", "value": "transaction.duration"}, + "type": "filter", + "value": { + "quoted": false, + "type": "valueText", + "value": ">..500s" + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/invalid_numeric_aggregate_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/invalid_numeric_aggregate_filter.json new file mode 100644 index 000000000..7d324fa86 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/invalid_numeric_aggregate_filter.json @@ -0,0 +1,15 @@ +[ + { + "query": "min(measurements.size):3s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "freeText", + "quoted": false, + "value": "min(measurements.size):3s", + "invalid": null + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/invalid_numeric_fields.json b/packages/toolkit-core/test/fixtures/search-syntax/invalid_numeric_fields.json new file mode 100644 index 000000000..e9db488c9 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/invalid_numeric_fields.json @@ -0,0 +1,122 @@ +[ + { + "query": "project.id:one", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "invalid-number", + "reason": "Invalid number. Expected number then optional k, m, or b suffix (e.g. 500k)", + "expectedType": ["numeric", "numericIn"] + }, + "key": {"type": "keySimple", "value": "project.id", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "one", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "issue.id:two", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "invalid-number", + "reason": "Invalid number. Expected number then optional k, m, or b suffix (e.g. 500k)", + "expectedType": ["numeric", "numericIn"] + }, + "key": {"type": "keySimple", "value": "issue.id", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "two", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "transaction.duration:>hotdog", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "invalid-duration", + "reason": "Invalid duration. Expected number followed by duration unit suffix", + "expectedType": ["duration"] + }, + "key": {"type": "keySimple", "value": "transaction.duration", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": ">hotdog", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "measurements.lcp:>hotdog", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "invalid-duration", + "reason": "Invalid duration. Expected number followed by duration unit suffix", + "expectedType": ["duration"] + }, + "key": {"type": "keySimple", "value": "measurements.lcp", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": ">hotdog", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "spans.db:>hotdog", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "invalid-duration", + "reason": "Invalid duration. Expected number followed by duration unit suffix", + "expectedType": ["duration"] + }, + "key": {"type": "keySimple", "value": "spans.db", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": ">hotdog", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/invalid_numeric_shorthand.json b/packages/toolkit-core/test/fixtures/search-syntax/invalid_numeric_shorthand.json new file mode 100644 index 000000000..a9f37cda5 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/invalid_numeric_shorthand.json @@ -0,0 +1,26 @@ +[ + { + "query": "stack.colno:>3s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "invalid-number", + "reason": "Invalid number. Expected number then optional k, m, or b suffix (e.g. 500k)", + "expectedType": ["numeric", "numericIn"] + }, + "key": {"type": "keySimple", "value": "stack.colno", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": ">3s", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/multiple_quotes.json b/packages/toolkit-core/test/fixtures/search-syntax/multiple_quotes.json new file mode 100644 index 000000000..264d9ebe6 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/multiple_quotes.json @@ -0,0 +1,66 @@ +[ + { + "query": "device.family:\"\" browser.name:\"Chrome\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "device.family", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "", + "quoted": true + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "browser.name", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "Chrome", + "quoted": true + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "device.family:\"\\\"\" browser.name:\"Chrome\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "device.family", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "\\\"", + "quoted": true + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "browser.name", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "Chrome", + "quoted": true + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/negated_duration_on_non_duration_field.json b/packages/toolkit-core/test/fixtures/search-syntax/negated_duration_on_non_duration_field.json new file mode 100644 index 000000000..fe60ce7c8 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/negated_duration_on_non_duration_field.json @@ -0,0 +1,21 @@ +[ + { + "query": "!user.id:500s", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": true, + "key": {"type": "keySimple", "value": "user.id", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "500s", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/negated_on_boolean_values_and_non_boolean_field.json b/packages/toolkit-core/test/fixtures/search-syntax/negated_on_boolean_values_and_non_boolean_field.json new file mode 100644 index 000000000..dd1e0abf0 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/negated_on_boolean_values_and_non_boolean_field.json @@ -0,0 +1,40 @@ +[ + { + "query": "!user.id:true", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": true, + "key": {"type": "keySimple", "value": "user.id", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "true", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!user.id:1", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": true, + "key": {"type": "keySimple", "value": "user.id", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "1", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/newline_outside_quote.json b/packages/toolkit-core/test/fixtures/search-syntax/newline_outside_quote.json new file mode 100644 index 000000000..a6c28f9d5 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/newline_outside_quote.json @@ -0,0 +1 @@ +[{"query": "release:a\nrelease", "raisesError": true, "result": []}] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/newline_within_quote.json b/packages/toolkit-core/test/fixtures/search-syntax/newline_within_quote.json new file mode 100644 index 000000000..6676d3e89 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/newline_within_quote.json @@ -0,0 +1,21 @@ +[ + { + "query": "release:\"a\nrelease\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "a\nrelease", + "quoted": true + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/not_has_tag.json b/packages/toolkit-core/test/fixtures/search-syntax/not_has_tag.json new file mode 100644 index 000000000..7427d4324 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/not_has_tag.json @@ -0,0 +1,42 @@ +[ + { + "desc": "unquoted key", + "query": "!has:release", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "has", + "negated": true, + "key": {"type": "keySimple", "value": "has", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "release", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "quoted key", + "query": "!has:\"hi:there\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "has", + "negated": true, + "key": {"type": "keySimple", "value": "has", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "hi:there", + "quoted": true + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/numeric_aggregate_measurements_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/numeric_aggregate_measurements_filter.json new file mode 100644 index 000000000..3ca68cec0 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/numeric_aggregate_measurements_filter.json @@ -0,0 +1,192 @@ +[ + { + "query": "min(measurements.size):3.1415", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "min", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "measurements.size", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": "", + "value": { + "type": "valueNumber", + "value": "3.1415", + "unit": null, + "parsed": {"value": 3.1415} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "min(measurements.size):>3.1415", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "min", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "measurements.size", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": ">", + "value": { + "type": "valueNumber", + "value": "3.1415", + "unit": null, + "parsed": {"value": 3.1415} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "min(measurements.size):<3.1415", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "min", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "measurements.size", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": "<", + "value": { + "type": "valueNumber", + "value": "3.1415", + "unit": null, + "parsed": {"value": 3.1415} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "min(measurements.size):<3k", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "min", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "measurements.size", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": "<", + "value": { + "type": "valueNumber", + "value": "3", + "unit": "k", + "parsed": {"value": 3000} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "min(measurements.size):2m", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "min", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "measurements.size", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": "", + "value": { + "type": "valueNumber", + "value": "2", + "unit": "m", + "parsed": {"value": 2000000} + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/numeric_aggregate_op_breakdowns_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/numeric_aggregate_op_breakdowns_filter.json new file mode 100644 index 000000000..e5ebdc673 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/numeric_aggregate_op_breakdowns_filter.json @@ -0,0 +1,283 @@ +[ + { + "query": "min(spans.browser):3.1415", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": { + "type": "keySimple", + "value": "min", + "quoted": false + }, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "spans.browser", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": { + "type": "spaces", + "value": "" + }, + "argsSpaceAfter": { + "type": "spaces", + "value": "" + } + }, + "operator": "", + "value": { + "type": "valueNumber", + "value": "3.1415", + "unit": null, + "parsed": { + "value": 3.1415 + } + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "query": "min(spans.browser):>3.1415", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": { + "type": "keySimple", + "value": "min", + "quoted": false + }, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "spans.browser", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": { + "type": "spaces", + "value": "" + }, + "argsSpaceAfter": { + "type": "spaces", + "value": "" + } + }, + "operator": ">", + "value": { + "type": "valueNumber", + "value": "3.1415", + "unit": null, + "parsed": { + "value": 3.1415 + } + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "query": "min(spans.browser):<3.1415", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": { + "type": "keySimple", + "value": "min", + "quoted": false + }, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "spans.browser", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": { + "type": "spaces", + "value": "" + }, + "argsSpaceAfter": { + "type": "spaces", + "value": "" + } + }, + "operator": "<", + "value": { + "type": "valueNumber", + "value": "3.1415", + "unit": null, + "parsed": { + "value": 3.1415 + } + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "query": "min(spans.browser):<3k", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": { + "type": "keySimple", + "value": "min", + "quoted": false + }, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "spans.browser", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": { + "type": "spaces", + "value": "" + }, + "argsSpaceAfter": { + "type": "spaces", + "value": "" + } + }, + "operator": "<", + "value": { + "type": "valueNumber", + "value": "3", + "unit": "k", + "parsed": { + "value": 3000 + } + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "query": "min(spans.browser):2m", + "desc": "XXX: Questionable if this should actually be parsed as a duration? This is how it was written when moved from the original python test", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "aggregateDuration", + "negated": false, + "key": { + "type": "keyAggregate", + "name": { + "type": "keySimple", + "value": "min", + "quoted": false + }, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "spans.browser", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": { + "type": "spaces", + "value": "" + }, + "argsSpaceAfter": { + "type": "spaces", + "value": "" + } + }, + "operator": "", + "value": { + "type": "valueDuration", + "value": "2", + "unit": "m", + "parsed": { + "value": 120000 + } + } + }, + { + "type": "spaces", + "value": "" + } + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/numeric_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/numeric_filter.json new file mode 100644 index 000000000..2cacb8028 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/numeric_filter.json @@ -0,0 +1,121 @@ +[ + { + "desc": "Numeric format should still return a string if field isn't allowed", + "query": "random_field:>500", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "random_field", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": ">500", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "Numeric format should not negate operator if field isn't allowed", + "query": "!random_field:>500", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": true, + "key": {"type": "keySimple", "value": "random_field", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": ">500", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!random_field:500", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": true, + "key": {"type": "keySimple", "value": "random_field", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "500", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "project_id:-500", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numeric", + "negated": false, + "key": {"type": "keySimple", "value": "project_id", "quoted": false}, + "operator": "", + "value": { + "type": "valueNumber", + "value": "-500", + "unit": null, + "parsed": {"value": -500} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!issue.id:500", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numeric", + "negated": true, + "key": {"type": "keySimple", "value": "issue.id", "quoted": false}, + "operator": "", + "value": { + "type": "valueNumber", + "value": "500", + "unit": null, + "parsed": {"value": 500} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "issue.id:<500", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numeric", + "negated": false, + "key": {"type": "keySimple", "value": "issue.id", "quoted": false}, + "operator": "<", + "value": { + "type": "valueNumber", + "value": "500", + "unit": null, + "parsed": {"value": 500} + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/numeric_filter_with_decimals.json b/packages/toolkit-core/test/fixtures/search-syntax/numeric_filter_with_decimals.json new file mode 100644 index 000000000..cfb71dd76 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/numeric_filter_with_decimals.json @@ -0,0 +1,24 @@ +[ + { + "query": "transaction.duration:>3.1415", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numeric", + "negated": false, + "key": {"type": "keySimple", "value": "transaction.duration", "quoted": false}, + "operator": ">", + "value": { + "type": "valueNumber", + "value": "3.1415", + "unit": null, + "parsed": { + "value": 3.1415 + } + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/numeric_filter_with_shorthand.json b/packages/toolkit-core/test/fixtures/search-syntax/numeric_filter_with_shorthand.json new file mode 100644 index 000000000..9a8865d78 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/numeric_filter_with_shorthand.json @@ -0,0 +1,62 @@ +[ + { + "query": "stack.colno:>3k", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numeric", + "negated": false, + "key": {"type": "keySimple", "value": "stack.colno", "quoted": false}, + "operator": ">", + "value": { + "type": "valueNumber", + "value": "3", + "unit": "k", + "parsed": {"value": 3000} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "stack.colno:>3m", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numeric", + "negated": false, + "key": {"type": "keySimple", "value": "stack.colno", "quoted": false}, + "operator": ">", + "value": { + "type": "valueNumber", + "value": "3", + "unit": "m", + "parsed": {"value": 3000000} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "stack.colno:>3b", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numeric", + "negated": false, + "key": {"type": "keySimple", "value": "stack.colno", "quoted": false}, + "operator": ">", + "value": { + "type": "valueNumber", + "value": "3", + "unit": "b", + "parsed": {"value": 3000000000} + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/numeric_in_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/numeric_in_filter.json new file mode 100644 index 000000000..b28347160 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/numeric_in_filter.json @@ -0,0 +1,414 @@ +[ + { + "query": "project_id:[500,501,502]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numericIn", + "negated": false, + "key": {"type": "keySimple", "value": "project_id", "quoted": false}, + "operator": "", + "value": { + "type": "valueNumberList", + "items": [ + { + "separator": "", + "value": { + "type": "valueNumber", + "value": "500", + "unit": null, + "parsed": { + "value": 500 + } + } + }, + { + "separator": ",", + "value": { + "type": "valueNumber", + "value": "501", + "unit": null, + "parsed": { + "value": 501 + } + } + }, + { + "separator": ",", + "value": { + "type": "valueNumber", + "value": "502", + "unit": null, + "parsed": { + "value": 502 + } + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "project_id:[500, 501, 502]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numericIn", + "negated": false, + "key": {"type": "keySimple", "value": "project_id", "quoted": false}, + "operator": "", + "value": { + "type": "valueNumberList", + "items": [ + { + "separator": "", + "value": { + "type": "valueNumber", + "value": "500", + "unit": null, + "parsed": { + "value": 500 + } + } + }, + { + "separator": ", ", + "value": { + "type": "valueNumber", + "value": "501", + "unit": null, + "parsed": { + "value": 501 + } + } + }, + { + "separator": ", ", + "value": { + "type": "valueNumber", + "value": "502", + "unit": null, + "parsed": { + "value": 502 + } + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "project_id:[500, 501 ,502]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numericIn", + "negated": false, + "key": {"type": "keySimple", "value": "project_id", "quoted": false}, + "operator": "", + "value": { + "type": "valueNumberList", + "items": [ + { + "separator": "", + "value": { + "type": "valueNumber", + "value": "500", + "unit": null, + "parsed": { + "value": 500 + } + } + }, + { + "separator": ", ", + "value": { + "type": "valueNumber", + "value": "501", + "unit": null, + "parsed": { + "value": 501 + } + } + }, + { + "separator": " ,", + "value": { + "type": "valueNumber", + "value": "502", + "unit": null, + "parsed": { + "value": 502 + } + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "project_id:[500,501,502] issue.id:[100]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numericIn", + "negated": false, + "key": {"type": "keySimple", "value": "project_id", "quoted": false}, + "operator": "", + "value": { + "type": "valueNumberList", + "items": [ + { + "separator": "", + "value": { + "type": "valueNumber", + "value": "500", + "unit": null, + "parsed": { + "value": 500 + } + } + }, + { + "separator": ",", + "value": { + "type": "valueNumber", + "value": "501", + "unit": null, + "parsed": { + "value": 501 + } + } + }, + { + "separator": ",", + "value": { + "type": "valueNumber", + "value": "502", + "unit": null, + "parsed": { + "value": 502 + } + } + } + ] + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "numericIn", + "negated": false, + "key": {"type": "keySimple", "value": "issue.id", "quoted": false}, + "operator": "", + "value": { + "type": "valueNumberList", + "items": [ + { + "separator": "", + "value": { + "type": "valueNumber", + "value": "100", + "unit": null, + "parsed": { + "value": 100 + } + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "Numeric format should still return a string if field isn't allowed", + "query": "project_id:[500,501,502] random_field:[500,501,502]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numericIn", + "negated": false, + "key": {"type": "keySimple", "value": "project_id", "quoted": false}, + "operator": "", + "value": { + "type": "valueNumberList", + "items": [ + { + "separator": "", + "value": { + "type": "valueNumber", + "value": "500", + "unit": null, + "parsed": { + "value": 500 + } + } + }, + { + "separator": ",", + "value": { + "type": "valueNumber", + "value": "501", + "unit": null, + "parsed": { + "value": 501 + } + } + }, + { + "separator": ",", + "value": { + "type": "valueNumber", + "value": "502", + "unit": null, + "parsed": { + "value": 502 + } + } + } + ] + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": {"type": "keySimple", "value": "random_field", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "500", + "quoted": false + } + }, + { + "separator": ",", + "value": { + "type": "valueText", + "value": "501", + "quoted": false + } + }, + { + "separator": ",", + "value": { + "type": "valueText", + "value": "502", + "quoted": false + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "project_id:[500,501,]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numericIn", + "invalid": { + "type": "empty-value-in-list-not-allowed", + "reason": "Lists should not have empty values" + }, + "negated": false, + "key": {"type": "keySimple", "value": "project_id", "quoted": false}, + "operator": "", + "value": { + "type": "valueNumberList", + "items": [ + { + "separator": "", + "value": { + "type": "valueNumber", + "value": "500", + "unit": null, + "parsed": { + "value": 500 + } + } + }, + { + "separator": ",", + "value": { + "type": "valueNumber", + "value": "501", + "unit": null, + "parsed": { + "value": 501 + } + } + }, + { + "separator": ",", + "value": null + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!project_id:[500,501]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numericIn", + "negated": true, + "key": {"type": "keySimple", "value": "project_id", "quoted": false}, + "operator": "", + "value": { + "type": "valueNumberList", + "items": [ + { + "separator": "", + "value": { + "type": "valueNumber", + "value": "500", + "unit": null, + "parsed": { + "value": 500 + } + } + }, + { + "separator": ",", + "value": { + "type": "valueNumber", + "value": "501", + "unit": null, + "parsed": { + "value": 501 + } + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/numeric_measurements_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/numeric_measurements_filter.json new file mode 100644 index 000000000..aaa5465ff --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/numeric_measurements_filter.json @@ -0,0 +1,68 @@ +[ + { + "query": "measurements.size:3.1415", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numeric", + "negated": false, + "key": {"type": "keySimple", "value": "measurements.size", "quoted": false}, + "operator": "", + "value": { + "type": "valueNumber", + "value": "3.1415", + "unit": null, + "parsed": { + "value": 3.1415 + } + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "measurements.size:>3.1415", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numeric", + "negated": false, + "key": {"type": "keySimple", "value": "measurements.size", "quoted": false}, + "operator": ">", + "value": { + "type": "valueNumber", + "value": "3.1415", + "unit": null, + "parsed": { + "value": 3.1415 + } + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "measurements.size:<3.1415", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "numeric", + "negated": false, + "key": {"type": "keySimple", "value": "measurements.size", "quoted": false}, + "operator": "<", + "value": { + "type": "valueNumber", + "value": "3.1415", + "unit": null, + "parsed": { + "value": 3.1415 + } + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/other_dates.json b/packages/toolkit-core/test/fixtures/search-syntax/other_dates.json new file mode 100644 index 000000000..ac822f052 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/other_dates.json @@ -0,0 +1,66 @@ +[ + { + "desc": "test date format with other name", + "query": "timestamp:>2015-05-18", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "date", + "negated": false, + "key": {"type": "keySimple", "value": "timestamp", "quoted": false}, + "operator": ">", + "value": { + "type": "valueIso8601Date", + "value": "2015-05-18", + "parsed": {"value": "2015-05-18T00:00:00.000Z"}, + "date": "2015-05-18", + "time": null, + "tz": null + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "timestamp.to_hour:>2018-01-01T05:06:07+00:00", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "date", + "negated": false, + "key": {"type": "keySimple", "value": "timestamp.to_hour", "quoted": false}, + "operator": ">", + "value": { + "type": "valueIso8601Date", + "value": "2018-01-01T05:06:07+00:00", + "parsed": {"value": "2018-01-01T05:06:07.000Z"}, + "date": "2018-01-01", + "time": "05:06:07", + "tz": "+00:00" + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "random:>2015-05-18", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "random", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": ">2015-05-18", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/quoted_free_text_search_anywhere.json b/packages/toolkit-core/test/fixtures/search-syntax/quoted_free_text_search_anywhere.json new file mode 100644 index 000000000..7022c0446 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/quoted_free_text_search_anywhere.json @@ -0,0 +1,41 @@ +[ + { + "query": "\"hello there\" user.email:foo@example.com \"general kenobi\"", + "result": [ + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "hello there", "quoted": true, "invalid": null}, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "user.email", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "foo@example.com", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + {"type": "freeText", "value": "general kenobi", "quoted": true, "invalid": null}, + {"type": "spaces", "value": ""} + ] + }, + { + "query": " \" hello \" ", + "result": [ + {"type": "spaces", "value": " "}, + {"type": "freeText", "value": " hello ", "quoted": true, "invalid": null}, + {"type": "spaces", "value": " "} + ] + }, + { + "query": " \" he\\\"llo \" ", + "result": [ + {"type": "spaces", "value": " "}, + {"type": "freeText", "value": " he\\\"llo ", "quoted": true, "invalid": null}, + {"type": "spaces", "value": " "} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/quoted_key.json b/packages/toolkit-core/test/fixtures/search-syntax/quoted_key.json new file mode 100644 index 000000000..89e8ffa82 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/quoted_key.json @@ -0,0 +1,40 @@ +[ + { + "query": "\"hi:there\":value", + "result": [ + {"type": "spaces", "value": ""}, + { + "filter": "text", + "negated": false, + "key": {"quoted": true, "type": "keySimple", "value": "hi:there"}, + "operator": "", + "type": "filter", + "value": { + "quoted": false, + "type": "valueText", + "value": "value" + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!\"hi:there\":value", + "result": [ + {"type": "spaces", "value": ""}, + { + "filter": "text", + "negated": true, + "key": {"quoted": true, "type": "keySimple", "value": "hi:there"}, + "operator": "", + "type": "filter", + "value": { + "quoted": false, + "type": "valueText", + "value": "value" + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/quoted_val.json b/packages/toolkit-core/test/fixtures/search-syntax/quoted_val.json new file mode 100644 index 000000000..fb7271381 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/quoted_val.json @@ -0,0 +1,228 @@ +[ + { + "query": "release:\"a release\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "a release", + "quoted": true + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!release:\"a release\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": true, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "a release", + "quoted": true + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "release:[\"a release\"]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "a release", + "quoted": true + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "release:[\"a release\",\"b release\"]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "a release", + "quoted": true + } + }, + { + "separator": ",", + "value": { + "type": "valueText", + "value": "b release", + "quoted": true + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "release:[\"a release\", \"b release\", \"c release\"]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "a release", + "quoted": true + } + }, + { + "separator": ", ", + "value": { + "type": "valueText", + "value": "b release", + "quoted": true + } + }, + { + "separator": ", ", + "value": { + "type": "valueText", + "value": "c release", + "quoted": true + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!release:[\"a release\",\"b release\"]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": true, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "a release", + "quoted": true + } + }, + { + "separator": ",", + "value": { + "type": "valueText", + "value": "b release", + "quoted": true + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "release:[\"a release\"] hello:[\"123\"]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "a release", + "quoted": true + } + } + ] + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": {"type": "keySimple", "value": "hello", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "123", + "quoted": true + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/quotes_filtered_on_raw.json b/packages/toolkit-core/test/fixtures/search-syntax/quotes_filtered_on_raw.json new file mode 100644 index 000000000..e67139289 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/quotes_filtered_on_raw.json @@ -0,0 +1,111 @@ +[ + { + "desc": "Enclose the full raw query? Strip it.", + "query": "thinger:unknown \"what is this?\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "thinger", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "unknown", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + {"type": "freeText", "value": "what is this?", "quoted": true, "invalid": null}, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "Enclose the full query? Strip it and the whole query is raw.", + "query": "\"thinger:unknown what is this?\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "freeText", + "value": "thinger:unknown what is this?", + "quoted": true, + "invalid": null + }, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "Allow a single quotation at end", + "query": "end\"", + "result": [ + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "end\"", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "Allow a single quotation at beginning", + "query": "\"beginning", + "result": [ + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "\"beginning", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "Allow a single quotation", + "query": "\"", + "result": [ + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "\"", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "Empty quotations become a dropped term", + "query": "\"\"", + "result": [ + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "", "quoted": true, "invalid": null}, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "Allow a search for space", + "query": "\" \"", + "result": [ + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": " ", "quoted": true, "invalid": null}, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "Strip in a balanced manner", + "query": "\"\"woof\"", + "result": [ + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "", "quoted": true, "invalid": null}, + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "woof\"", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "Don't try this at home kids", + "query": "\"\"\"\"\"\"\"\"\"", + "result": [ + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "", "quoted": true, "invalid": null}, + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "", "quoted": true, "invalid": null}, + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "", "quoted": true, "invalid": null}, + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "", "quoted": true, "invalid": null}, + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "\"", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/regex_empty_pattern.json b/packages/toolkit-core/test/fixtures/search-syntax/regex_empty_pattern.json new file mode 100644 index 000000000..499958c55 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/regex_empty_pattern.json @@ -0,0 +1,84 @@ +[ + { + "desc": "An empty pattern is flagged rather than searched for", + "query": "message:////", + "additionalConfig": { + "allowRegex": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "text", + "key": { + "type": "keySimple", + "value": "message", + "quoted": false + }, + "value": { + "type": "valueText", + "value": "", + "quoted": false + }, + "negated": false, + "operator": "//", + "invalid": { + "type": "filter-must-have-value", + "reason": "Filter must have a value" + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "An empty pattern on an array membership key is flagged too", + "query": "tags[csv_headers[*],array]:////", + "additionalConfig": { + "allowRegex": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "arrayIncludes", + "key": { + "type": "keyArrayIncludes", + "key": { + "type": "keyExplicitArrayTag", + "prefix": "tags", + "key": { + "type": "keySimple", + "value": "csv_headers", + "quoted": false + } + }, + "index": "*" + }, + "value": { + "type": "valueText", + "value": "", + "quoted": false + }, + "negated": false, + "operator": "//", + "invalid": { + "type": "filter-must-have-value", + "reason": "Filter must have a value" + } + }, + { + "type": "spaces", + "value": "" + } + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/regex_filter_as_literal.json b/packages/toolkit-core/test/fixtures/search-syntax/regex_filter_as_literal.json new file mode 100644 index 000000000..f44c5ebc0 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/regex_filter_as_literal.json @@ -0,0 +1,41 @@ +[ + { + "query": "message://GET (api) v2// level:error", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "message", "quoted": false}, + "operator": "", + "value": {"type": "valueText", "value": "//GET (api) v2//", "quoted": false} + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "level", "quoted": false}, + "operator": "", + "value": {"type": "valueText", "value": "error", "quoted": false} + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!message://a//b//", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": true, + "key": {"type": "keySimple", "value": "message", "quoted": false}, + "operator": "", + "value": {"type": "valueText", "value": "//a//b//", "quoted": false} + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/regex_operator.json b/packages/toolkit-core/test/fixtures/search-syntax/regex_operator.json new file mode 100644 index 000000000..c6dfbe329 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/regex_operator.json @@ -0,0 +1,411 @@ +[ + { + "desc": "Spaces and parens need no quoting inside a pattern", + "query": "message://GET (api) v2//", + "additionalConfig": { + "allowRegex": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "text", + "key": { + "type": "keySimple", + "value": "message", + "quoted": false + }, + "value": { + "type": "valueText", + "value": "GET (api) v2", + "quoted": false + }, + "negated": false, + "operator": "//" + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "A leading ! negates the whole regex filter", + "query": "!message://^a.*b//", + "additionalConfig": { + "allowRegex": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "text", + "key": { + "type": "keySimple", + "value": "message", + "quoted": false + }, + "value": { + "type": "valueText", + "value": "^a.*b", + "quoted": false + }, + "negated": true, + "operator": "//" + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "The pattern runs to the first // that ends the value, so inner // are literal", + "query": "message://a//b//", + "additionalConfig": { + "allowRegex": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "text", + "key": { + "type": "keySimple", + "value": "message", + "quoted": false + }, + "value": { + "type": "valueText", + "value": "a//b", + "quoted": false + }, + "negated": false, + "operator": "//" + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "A space after the closing // ends the pattern", + "query": "message://a// level:error", + "additionalConfig": { + "allowRegex": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "text", + "key": { + "type": "keySimple", + "value": "message", + "quoted": false + }, + "value": { + "type": "valueText", + "value": "a", + "quoted": false + }, + "negated": false, + "operator": "//" + }, + { + "type": "spaces", + "value": " " + }, + { + "type": "filter", + "filter": "text", + "key": { + "type": "keySimple", + "value": "level", + "quoted": false + }, + "value": { + "type": "valueText", + "value": "error", + "quoted": false + }, + "negated": false, + "operator": "" + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "Array membership keys accept a pattern", + "query": "tags[csv_headers[*],array]://^a b//", + "additionalConfig": { + "allowRegex": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "arrayIncludes", + "key": { + "type": "keyArrayIncludes", + "key": { + "type": "keyExplicitArrayTag", + "prefix": "tags", + "key": { + "type": "keySimple", + "value": "csv_headers", + "quoted": false + } + }, + "index": "*" + }, + "value": { + "type": "valueText", + "value": "^a b", + "quoted": false + }, + "negated": false, + "operator": "//" + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "A quoted //...// stays a literal value", + "query": "message:\"//a//\"", + "additionalConfig": { + "allowRegex": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "text", + "key": { + "type": "keySimple", + "value": "message", + "quoted": false + }, + "value": { + "type": "valueText", + "value": "//a//", + "quoted": true + }, + "negated": false, + "operator": "" + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "Without a terminator after the closing //, the value stays literal", + "query": "message://a b//x", + "additionalConfig": { + "allowRegex": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "text", + "key": { + "type": "keySimple", + "value": "message", + "quoted": false + }, + "value": { + "type": "valueText", + "value": "//a", + "quoted": false + }, + "negated": false, + "operator": "" + }, + { + "type": "spaces", + "value": " " + }, + { + "type": "freeText", + "value": "b//x", + "quoted": false, + "invalid": null + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "A pattern at the length limit is accepted", + "query": "message://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa//", + "additionalConfig": { + "allowRegex": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "text", + "key": { + "type": "keySimple", + "value": "message", + "quoted": false + }, + "value": { + "type": "valueText", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "quoted": false + }, + "negated": false, + "operator": "//" + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "A pattern over the length limit is rejected", + "query": "message://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa//", + "additionalConfig": { + "allowRegex": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "text", + "key": { + "type": "keySimple", + "value": "message", + "quoted": false + }, + "value": { + "type": "valueText", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "quoted": false + }, + "negated": false, + "operator": "//", + "invalid": { + "type": "regex-pattern-too-long", + "reason": "Regex patterns are limited to 64 characters. To search for a literal value that starts with //, quote it: \"//...\"" + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "A pattern within the length limit when each escape sequence counts as one character is accepted", + "query": "message://^(https?:\\/\\/)?(www\\.)?([a-zA-Z0-9-]+)(\\.[a-zA-Z]{2,})(\\/[^\\s]*)?$//", + "additionalConfig": { + "allowRegex": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "text", + "key": { + "type": "keySimple", + "value": "message", + "quoted": false + }, + "value": { + "type": "valueText", + "value": "^(https?:\\/\\/)?(www\\.)?([a-zA-Z0-9-]+)(\\.[a-zA-Z]{2,})(\\/[^\\s]*)?$", + "quoted": false + }, + "negated": false, + "operator": "//" + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "desc": "A pattern over the length limit when each escape sequence counts as one character is rejected", + "query": "message://\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.//", + "additionalConfig": { + "allowRegex": true + }, + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "text", + "key": { + "type": "keySimple", + "value": "message", + "quoted": false + }, + "value": { + "type": "valueText", + "value": "\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.\\.", + "quoted": false + }, + "negated": false, + "operator": "//", + "invalid": { + "type": "regex-pattern-too-long", + "reason": "Regex patterns are limited to 64 characters. To search for a literal value that starts with //, quote it: \"//...\"" + } + }, + { + "type": "spaces", + "value": "" + } + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/rel_time_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/rel_time_filter.json new file mode 100644 index 000000000..2ba3dfc22 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/rel_time_filter.json @@ -0,0 +1,97 @@ +[ + { + "query": "firstSeen:+7d", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "relativeDate", + "negated": false, + "key": { + "type": "keySimple", + "value": "firstSeen", + "quoted": false + }, + "operator": "", + "value": { + "type": "valueRelativeDate", + "value": "7", + "sign": "+", + "unit": "d", + "parsed": { + "value": "2017-10-24T02:41:20.000Z" + } + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "query": "firstSeen:-2w", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "relativeDate", + "negated": false, + "key": { + "type": "keySimple", + "value": "firstSeen", + "quoted": false + }, + "operator": "", + "value": { + "type": "valueRelativeDate", + "value": "2", + "sign": "-", + "unit": "w", + "parsed": { + "value": "2017-10-03T02:41:20.000Z" + } + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "query": "random:-2w", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": { + "type": "keySimple", + "value": "random", + "quoted": false + }, + "operator": "", + "value": { + "type": "valueText", + "value": "-2w", + "quoted": false + } + }, + { + "type": "spaces", + "value": "" + } + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/semver.json b/packages/toolkit-core/test/fixtures/search-syntax/semver.json new file mode 100644 index 000000000..5f210b6fd --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/semver.json @@ -0,0 +1,78 @@ +[ + { + "query": "release.version:>1.2.3", + "result": [ + {"type": "spaces", "value": ""}, + { + "filter": "text", + "key": {"quoted": false, "type": "keySimple", "value": "release.version"}, + "negated": false, + "operator": ">", + "type": "filter", + "value": { + "quoted": false, + "type": "valueText", + "value": "1.2.3" + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "release.version:>1.2.3-hi", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "key": {"quoted": false, "type": "keySimple", "value": "release.version"}, + "negated": false, + "operator": ">", + "value": { + "quoted": false, + "type": "valueText", + "value": "1.2.3-hi" + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "release.version:>=1.2.3-hi", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "key": {"quoted": false, "type": "keySimple", "value": "release.version"}, + "negated": false, + "operator": ">=", + "value": { + "quoted": false, + "type": "valueText", + "value": "1.2.3-hi" + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "release.version:1.2.3-hi", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "key": {"quoted": false, "type": "keySimple", "value": "release.version"}, + "negated": false, + "operator": "", + "value": { + "quoted": false, + "type": "valueText", + "value": "1.2.3-hi" + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/simple.json b/packages/toolkit-core/test/fixtures/search-syntax/simple.json new file mode 100644 index 000000000..e3642011a --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/simple.json @@ -0,0 +1,71 @@ +[ + { + "desc": "test with raw search query at the end", + "query": "user.email:foo@example.com release:1.2.1 hello", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "user.email", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "foo@example.com", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "1.2.1", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + {"type": "freeText", "value": "hello", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "hello user.email:foo@example.com release:1.2.1", + "result": [ + {"type": "spaces", "value": ""}, + {"type": "freeText", "value": "hello ", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "user.email", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "foo@example.com", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "1.2.1", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/simple_in.json b/packages/toolkit-core/test/fixtures/search-syntax/simple_in.json new file mode 100644 index 000000000..c94f00aed --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/simple_in.json @@ -0,0 +1,496 @@ +[ + { + "query": "user.email:[test@test.com] test:[hello]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": {"type": "keySimple", "value": "user.email", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "test@test.com", + "quoted": false + } + } + ] + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": {"type": "keySimple", "value": "test", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "hello", + "quoted": false + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "user.email:[test@test.com,test2@test.com,test3@test.com] test:[hello]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": {"type": "keySimple", "value": "user.email", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "test@test.com", + "quoted": false + } + }, + { + "separator": ",", + "value": { + "type": "valueText", + "value": "test2@test.com", + "quoted": false + } + }, + { + "separator": ",", + "value": { + "type": "valueText", + "value": "test3@test.com", + "quoted": false + } + } + ] + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": {"type": "keySimple", "value": "test", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "hello", + "quoted": false + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!user.email:[test@test.com, test@test2.com, test@test3.com] test:[hello]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": true, + "key": {"type": "keySimple", "value": "user.email", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "test@test.com", + "quoted": false + } + }, + { + "separator": ", ", + "value": { + "type": "valueText", + "value": "test@test2.com", + "quoted": false + } + }, + { + "separator": ", ", + "value": { + "type": "valueText", + "value": "test@test3.com", + "quoted": false + } + } + ] + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": {"type": "keySimple", "value": "test", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "hello", + "quoted": false + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "test:h[e]llo]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "test", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "h[e]llo]", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "test:[h[e]llo", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "test", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "[h[e]llo", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "test:\"[h]\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "test", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "[h]", + "quoted": true + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "test:[h]*", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "test", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "[h]*", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "test:[h e]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "test", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "[h", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + {"type": "freeText", "value": "e]", "quoted": false, "invalid": null}, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "test:[]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "test", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "[]", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "user.email:[test@test.com, \"hi\", 1]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": {"type": "keySimple", "value": "user.email", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "test@test.com", + "quoted": false + } + }, + { + "separator": ", ", + "value": { + "type": "valueText", + "value": "hi", + "quoted": true + } + }, + { + "separator": ", ", + "value": { + "type": "valueText", + "value": "1", + "quoted": false + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "user.email:[test@test.com, \"hi\", 1.0]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": {"type": "keySimple", "value": "user.email", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "test@test.com", + "quoted": false + } + }, + { + "separator": ", ", + "value": { + "type": "valueText", + "value": "hi", + "quoted": true + } + }, + { + "separator": ", ", + "value": { + "type": "valueText", + "value": "1.0", + "quoted": false + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "test:[[h]]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": {"type": "keySimple", "value": "test", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "[h]", + "quoted": false + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "test:[a, [h]]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": {"type": "keySimple", "value": "test", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "a", + "quoted": false + } + }, + { + "separator": ", ", + "value": { + "type": "valueText", + "value": "[h]", + "quoted": false + } + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "user.email:[test@test.com]user.email:hello@hello.com", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "user.email", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "[test@test.com]user.email:hello@hello.com", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "user.email:[test@test.com, ]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "invalid": { + "type": "empty-value-in-list-not-allowed", + "reason": "Lists should not have empty values" + }, + "negated": false, + "key": {"type": "keySimple", "value": "user.email", "quoted": false}, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": { + "type": "valueText", + "value": "test@test.com", + "quoted": false + } + }, + { + "separator": ", ", + "value": null + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/sooo_many_quotes.json b/packages/toolkit-core/test/fixtures/search-syntax/sooo_many_quotes.json new file mode 100644 index 000000000..368151deb --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/sooo_many_quotes.json @@ -0,0 +1,21 @@ +[ + { + "query": "device.family:\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "device.family", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"", + "quoted": true + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/special_character_params.json b/packages/toolkit-core/test/fixtures/search-syntax/special_character_params.json new file mode 100644 index 000000000..f3e1b11de --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/special_character_params.json @@ -0,0 +1,173 @@ +[ + { + "query": "count_if(user.email,equals,test+something@example.com):>100", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "count_if", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "user.email", + "quoted": false + } + }, + { + "separator": ",", + "value": { + "type": "keyAggregateParam", + "value": "equals", + "quoted": false + } + }, + { + "separator": ",", + "value": { + "type": "keyAggregateParam", + "value": "test+something@example.com", + "quoted": false + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": ">", + "value": { + "type": "valueNumber", + "unit": null, + "value": "100", + "parsed": {"value": 100} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "count_if(user.email,equals,\"something,with,commas\"):>100", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "count_if", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "user.email", + "quoted": false + } + }, + { + "separator": ",", + "value": { + "type": "keyAggregateParam", + "value": "equals", + "quoted": false + } + }, + { + "separator": ",", + "value": { + "type": "keyAggregateParam", + "value": "\"something,with,commas\"", + "quoted": true + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": ">", + "value": { + "type": "valueNumber", + "unit": null, + "value": "100", + "parsed": {"value": 100} + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "count_if(user.email,equals,\"I, got my own \\\"quotes\\\"\"):>100", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "aggregateNumeric", + "negated": false, + "key": { + "type": "keyAggregate", + "name": {"type": "keySimple", "value": "count_if", "quoted": false}, + "args": { + "type": "keyAggregateArgs", + "args": [ + { + "separator": "", + "value": { + "type": "keyAggregateParam", + "value": "user.email", + "quoted": false + } + }, + { + "separator": ",", + "value": { + "type": "keyAggregateParam", + "value": "equals", + "quoted": false + } + }, + { + "separator": ",", + "value": { + "type": "keyAggregateParam", + "value": "\"I, got my own \\\"quotes\\\"\"", + "quoted": true + } + } + ] + }, + "argsSpaceBefore": {"type": "spaces", "value": ""}, + "argsSpaceAfter": {"type": "spaces", "value": ""} + }, + "operator": ">", + "value": { + "type": "valueNumber", + "unit": null, + "value": "100", + "parsed": {"value": 100} + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/specific_time_filter.json b/packages/toolkit-core/test/fixtures/search-syntax/specific_time_filter.json new file mode 100644 index 000000000..ad18fd476 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/specific_time_filter.json @@ -0,0 +1,131 @@ +[ + { + "query": "firstSeen:2018-01-01", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "specificDate", + "negated": false, + "key": { + "type": "keySimple", + "value": "firstSeen", + "quoted": false + }, + "operator": "", + "value": { + "type": "valueIso8601Date", + "value": "2018-01-01", + "parsed": {"value": "2018-01-01T00:00:00.000Z"}, + "date": "2018-01-01", + "time": null, + "tz": null + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "query": "lastSeen:2018-01-01T05:06:07Z", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "specificDate", + "negated": false, + "key": { + "type": "keySimple", + "value": "lastSeen", + "quoted": false + }, + "operator": "", + "value": { + "type": "valueIso8601Date", + "value": "2018-01-01T05:06:07Z", + "parsed": { + "value": "2018-01-01T05:06:07.000Z" + }, + "date": "2018-01-01", + "time": "05:06:07", + "tz": "Z" + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "query": "firstSeen:2018-01-01T05:06:07+00:00", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "specificDate", + "negated": false, + "key": { + "type": "keySimple", + "value": "firstSeen", + "quoted": false + }, + "operator": "", + "value": { + "type": "valueIso8601Date", + "value": "2018-01-01T05:06:07+00:00", + "parsed": { + "value": "2018-01-01T05:06:07.000Z" + }, + "date": "2018-01-01", + "time": "05:06:07", + "tz": "+00:00" + } + }, + { + "type": "spaces", + "value": "" + } + ] + }, + { + "query": "random:2018-01-01T05:06:07", + "result": [ + { + "type": "spaces", + "value": "" + }, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": { + "type": "keySimple", + "value": "random", + "quoted": false + }, + "operator": "", + "value": { + "type": "valueText", + "value": "2018-01-01T05:06:07", + "quoted": false + } + }, + { + "type": "spaces", + "value": "" + } + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/starts_with_operator.json b/packages/toolkit-core/test/fixtures/search-syntax/starts_with_operator.json new file mode 100644 index 000000000..913534d24 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/starts_with_operator.json @@ -0,0 +1,86 @@ +[ + { + "query": "browser.name:StartsWithfirefox", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "operator": "StartsWith", + "key": {"type": "keySimple", "value": "browser.name", "quoted": false}, + "value": {"type": "valueText", "value": "firefox", "quoted": false} + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "browser.name:StartsWith[firefox, chrome]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "operator": "StartsWith", + "key": {"type": "keySimple", "value": "browser.name", "quoted": false}, + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": {"type": "valueText", "value": "firefox", "quoted": false} + }, + { + "separator": ", ", + "value": {"type": "valueText", "value": "chrome", "quoted": false} + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!browser.name:StartsWithfirefox", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": true, + "operator": "StartsWith", + "key": {"type": "keySimple", "value": "browser.name", "quoted": false}, + "value": {"type": "valueText", "value": "firefox", "quoted": false} + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "!browser.name:StartsWith[firefox, chrome]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": true, + "operator": "StartsWith", + "key": {"type": "keySimple", "value": "browser.name", "quoted": false}, + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": {"type": "valueText", "value": "firefox", "quoted": false} + }, + { + "separator": ", ", + "value": {"type": "valueText", "value": "chrome", "quoted": false} + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/supported_tags.json b/packages/toolkit-core/test/fixtures/search-syntax/supported_tags.json new file mode 100644 index 000000000..c35b4fe2f --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/supported_tags.json @@ -0,0 +1,97 @@ +[ + { + "desc": "test with unsupported text field", + "query": "browser:\"chrome\" os:*windows*", + "additionalConfig": { + "validateKeys": true, + "supportedTags": { + "browser": { + "key": "browser", + "kind": "field" + } + } + }, + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "browser", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "chrome", + "quoted": true + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "invalid-key", + "reason": "Invalid key. \"os\" is not a supported search key." + }, + "key": {"type": "keySimple", "value": "os", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "*windows*", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "test with unsupported numeric field", + "query": "browser:\"chrome\" transaction.duration:>0", + "additionalConfig": { + "validateKeys": true, + "supportedTags": { + "browser": { + "key": "browser", + "kind": "field" + } + } + }, + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "browser", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "chrome", + "quoted": true + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "numeric", + "negated": false, + "invalid": { + "type": "invalid-key", + "reason": "Invalid key. \"transaction.duration\" is not a supported search key." + }, + "key": {"type": "keySimple", "value": "transaction.duration", "quoted": false}, + "operator": ">", + "value": { + "type": "valueNumber", + "value": "0", + "unit": null, + "parsed": { + "value": 0 + } + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/tab_outside_quote.json b/packages/toolkit-core/test/fixtures/search-syntax/tab_outside_quote.json new file mode 100644 index 000000000..47cb8b903 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/tab_outside_quote.json @@ -0,0 +1,24 @@ +[ + { + "desc": "tab outside quote", + "query": "release:a\trelease", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "a", + "quoted": false + } + }, + {"type": "spaces", "value": ""}, + {"quoted": false, "type": "freeText", "value": "\trelease", "invalid": null}, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/tab_within_quote.json b/packages/toolkit-core/test/fixtures/search-syntax/tab_within_quote.json new file mode 100644 index 000000000..d5ed0ed67 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/tab_within_quote.json @@ -0,0 +1,21 @@ +[ + { + "query": "release:\"a\trelease\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "a\trelease", + "quoted": true + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/timestamp.json b/packages/toolkit-core/test/fixtures/search-syntax/timestamp.json new file mode 100644 index 000000000..8e0176c74 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/timestamp.json @@ -0,0 +1,94 @@ +[ + { + "desc": "test date format", + "query": "timestamp:>2015-05-18", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "date", + "negated": false, + "key": {"type": "keySimple", "value": "timestamp", "quoted": false}, + "operator": ">", + "value": { + "type": "valueIso8601Date", + "value": "2015-05-18", + "parsed": {"value": "2015-05-18T00:00:00.000Z"}, + "date": "2015-05-18", + "time": null, + "tz": null + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "test date time format", + "query": "timestamp:>2015-05-18T10:15:01", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "date", + "negated": false, + "key": {"type": "keySimple", "value": "timestamp", "quoted": false}, + "operator": ">", + "value": { + "type": "valueIso8601Date", + "value": "2015-05-18T10:15:01", + "parsed": {"value": "2015-05-18T10:15:01.000Z"}, + "date": "2015-05-18", + "time": "10:15:01", + "tz": null + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "test date time format w microseconds", + "query": "timestamp:>2015-05-18T10:15:01.103", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "date", + "negated": false, + "key": {"type": "keySimple", "value": "timestamp", "quoted": false}, + "operator": ">", + "value": { + "type": "valueIso8601Date", + "value": "2015-05-18T10:15:01.103", + "parsed": {"value": "2015-05-18T10:15:01.103Z"}, + "date": "2015-05-18", + "time": "10:15:01.103", + "tz": null + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "desc": "test date time format w microseconds and utc marker", + "query": "timestamp:>2015-05-18T10:15:01.103Z", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "date", + "negated": false, + "key": {"type": "keySimple", "value": "timestamp", "quoted": false}, + "operator": ">", + "value": { + "type": "valueIso8601Date", + "value": "2015-05-18T10:15:01.103Z", + "parsed": {"value": "2015-05-18T10:15:01.103Z"}, + "date": "2015-05-18", + "time": "10:15:01.103", + "tz": "Z" + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/timestamp_rollup.json b/packages/toolkit-core/test/fixtures/search-syntax/timestamp_rollup.json new file mode 100644 index 000000000..66211b15d --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/timestamp_rollup.json @@ -0,0 +1,24 @@ +[ + { + "query": "timestamp.to_hour:2018-01-01T05:06:07+00:00", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "specificDate", + "negated": false, + "key": {"type": "keySimple", "value": "timestamp.to_hour", "quoted": false}, + "operator": "", + "value": { + "type": "valueIso8601Date", + "value": "2018-01-01T05:06:07+00:00", + "parsed": {"value": "2018-01-01T05:06:07.000Z"}, + "date": "2018-01-01", + "time": "05:06:07", + "tz": "+00:00" + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/packages/toolkit-core/test/fixtures/search-syntax/trailing_quote_value.json b/packages/toolkit-core/test/fixtures/search-syntax/trailing_quote_value.json new file mode 100644 index 000000000..ebaf191d9 --- /dev/null +++ b/packages/toolkit-core/test/fixtures/search-syntax/trailing_quote_value.json @@ -0,0 +1,120 @@ +[ + { + "query": "device.family:\"test", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "must-be-quoted", + "reason": "Quotes must enclose text or be escaped" + }, + "key": {"type": "keySimple", "value": "device.family", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "\"test", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "url:test\"", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "must-be-quoted", + "reason": "Quotes must enclose text or be escaped" + }, + "key": {"type": "keySimple", "value": "url", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "test\"", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "url:\"test transaction:abadcafe", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "must-be-quoted", + "reason": "Quotes must enclose text or be escaped" + }, + "key": {"type": "keySimple", "value": "url", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "\"test", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "transaction", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "abadcafe", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "url:te\"st transaction:abadcafe", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "invalid": { + "type": "must-be-quoted", + "reason": "Quotes must enclose text or be escaped" + }, + "key": {"type": "keySimple", "value": "url", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "te\"st", + "quoted": false + } + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "transaction", "quoted": false}, + "operator": "", + "value": { + "type": "valueText", + "value": "abadcafe", + "quoted": false + } + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98d5910dd..2bd54f343 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -500,9 +500,6 @@ importers: p-limit: specifier: ^7.3.0 version: 7.3.2 - peggy: - specifier: ^5.1.0 - version: 5.1.0 picomatch: specifier: ^4.0.4 version: 4.0.5 @@ -936,6 +933,9 @@ importers: packages/toolkit-core: devDependencies: + peggy: + specifier: ^5.1.0 + version: 5.1.0 typescript: specifier: 'catalog:' version: 5.8.3 diff --git a/scripts/pre-commit-generated.mjs b/scripts/pre-commit-generated.mjs index 2cba9b4e6..49ffccf3c 100644 --- a/scripts/pre-commit-generated.mjs +++ b/scripts/pre-commit-generated.mjs @@ -26,6 +26,18 @@ const GENERATED_TASKS = [ "plugins/sentry-mcp-experimental/agents/sentry-mcp.md", ], }, + { + name: "toolkit-core grammar parsers", + command: ["pnpm", "--filter", "@sentry/toolkit-core", "generate"], + inputs: [ + /^packages\/toolkit-core\/grammar\/.*\.pegjs$/, + /^packages\/toolkit-core\/script\/generate-parsers\.ts$/, + ], + outputs: [ + "packages/toolkit-core/src/generated/search-syntax-grammar.js", + "packages/toolkit-core/src/generated/search-syntax-grammar.d.ts", + ], + }, ]; function run(command) { From 8dd4546527200aa2477ba3ebb8db2305079d2ba4 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:55:06 +0000 Subject: [PATCH 2/4] fix(cli): Don't merge OR'd filters that have no in-list form Aggregate (count():10), array-includes (foo[*]:a), and other typed filters have no in-list rule in Sentry's grammar, so rewriting 'count():10 OR count():5' to 'count():[10,5]' produced an invalid query. Only text and numeric filters are now mergeable; the rest stay opaque and the OR is rejected as before. Co-Authored-By: Burak Yigit Kaya --- packages/cli/src/lib/search-query.ts | 15 ++++++++++++++- packages/cli/test/lib/search-query.test.ts | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/lib/search-query.ts b/packages/cli/src/lib/search-query.ts index 1d1ee2999..92984feca 100644 --- a/packages/cli/src/lib/search-query.ts +++ b/packages/cli/src/lib/search-query.ts @@ -26,6 +26,7 @@ import { type FilterToken, + FilterType, type ParseResultToken, parseSearch, TermOperator, @@ -122,6 +123,18 @@ export type SearchNode = | OpaqueTerm; const COMPARISON_PREFIX_RE = /^(>=|<=|!=|>|<|=)/; + +/** + * Filter types whose key shape also has an in-list rule in Sentry's grammar + * (`text_in_filter` / `numeric_in_filter`). Aggregate (`count():10`), + * array-includes (`foo[*]:a`), `has:`/`is:`, boolean, date, duration and + * size filters have no in-list form, so merging them would build a query + * the API rejects. + */ +const LISTABLE_FILTER_TYPES: ReadonlySet = new Set([ + FilterType.TEXT, + FilterType.NUMERIC, +]); const FREE_TEXT_SEPARATOR_RE = /[ \t]+/; type Located = { @@ -172,7 +185,7 @@ function filterToNode(query: string, token: FilterToken): SearchNode { }; } - if (!value) { + if (!value || !LISTABLE_FILTER_TYPES.has(token.filter)) { return { type: "opaque", raw: sourceOf(query, token) }; } diff --git a/packages/cli/test/lib/search-query.test.ts b/packages/cli/test/lib/search-query.test.ts index f42acfc45..b5eaf9438 100644 --- a/packages/cli/test/lib/search-query.test.ts +++ b/packages/cli/test/lib/search-query.test.ts @@ -594,6 +594,23 @@ describe("sanitizeQuery: shared grammar behavior", () => { ); }); + test("refuses to merge filters with no in-list form", () => { + for (const query of [ + "count():10 OR count():5", + "p95(span.duration):10 OR p95(span.duration):5", + "foo[*]:a OR foo[*]:b", + "tags[k[*],array]:a OR tags[k[*],array]:b", + ]) { + expect(() => sanitizeQuery(query), query).toThrow(ValidationError); + } + }); + + test("passes filters with no in-list form through when there is no OR", () => { + expect(sanitizeQuery("count():10 foo[*]:a level:error")).toBe( + "count():10 foo[*]:a level:error", + ); + }); + test("strips AND between multi-word free text and filters", () => { expect(sanitizeQuery("database timeout AND level:error")).toBe( "database timeout level:error", From a010a3640db162c5e5ef5da06123974bfa1d49f2 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:58:29 +0000 Subject: [PATCH 3/4] test(core): Move grammar-level regex tests into toolkit-core The CLI and MCP each tested how Sentry's grammar reads regex patterns and key shapes. That's the shared parser's job now, so those cases live in one table in search-syntax.test.ts. The CLI keeps the sanitizer tests (AND stripping and OR rewriting around patterns) and MCP keeps its detection heuristics. --- packages/cli/test/lib/search-query.test.ts | 85 +------------- .../tools/support/search-events/utils.test.ts | 18 +-- .../toolkit-core/src/search-syntax.test.ts | 107 ++++++++++++++++++ 3 files changed, 113 insertions(+), 97 deletions(-) diff --git a/packages/cli/test/lib/search-query.test.ts b/packages/cli/test/lib/search-query.test.ts index b5eaf9438..7d0bf9d1e 100644 --- a/packages/cli/test/lib/search-query.test.ts +++ b/packages/cli/test/lib/search-query.test.ts @@ -402,52 +402,15 @@ describe("sanitizeQuery: edge cases", () => { }); describe("sanitizeQuery: regex filters", () => { - test("passes through a character class containing a comma", () => { - expect(sanitizeQuery("message://[a-z,]+//")).toBe("message://[a-z,]+//"); - }); - - test("passes through a malformed-looking list inside a pattern", () => { - expect(sanitizeQuery("message://codes [401,403,) seen//")).toBe( - "message://codes [401,403,) seen//", - ); - }); - - test("does not treat OR inside a pattern as a boolean operator", () => { - expect(sanitizeQuery("message://timeout OR refused//")).toBe( - "message://timeout OR refused//", - ); - }); - - test("does not treat AND inside a pattern as a boolean operator", () => { - expect(sanitizeQuery("message://read AND write//")).toBe( - "message://read AND write//", - ); - }); - + // How the grammar reads regex patterns and keys is tested in + // packages/toolkit-core/src/search-syntax.test.ts. These cover the + // sanitizer: patterns survive AND stripping and OR rewriting untouched. test("preserves repeated whitespace inside a pattern", () => { expect(sanitizeQuery("message://a AND b//")).toBe( "message://a AND b//", ); }); - test("passes through a negated regex filter", () => { - expect(sanitizeQuery("!message://^GET OR POST \\d+ms//")).toBe( - "!message://^GET OR POST \\d+ms//", - ); - }); - - test("passes through parentheses inside a pattern", () => { - expect(sanitizeQuery("message://(reset|timeout) [0-9,]+//")).toBe( - "message://(reset|timeout) [0-9,]+//", - ); - }); - - test("passes through a double quote inside a pattern", () => { - expect(sanitizeQuery('message://say "hi" [a,]//')).toBe( - 'message://say "hi" [a,]//', - ); - }); - test("ends the pattern at the first // followed by whitespace", () => { expect(sanitizeQuery("url://https://x// AND message://a OR b//")).toBe( "url://https://x// message://a OR b//", @@ -476,48 +439,6 @@ describe("sanitizeQuery: regex filters", () => { ); }); - test("passes through a regex on a typed tag key", () => { - expect(sanitizeQuery("tags[foo,string]://[a-z,]+ AND b//")).toBe( - "tags[foo,string]://[a-z,]+ AND b//", - ); - }); - - test("passes through a regex on a typed tag key with spaces", () => { - expect(sanitizeQuery("tags[foo, string]://a OR [b,]//")).toBe( - "tags[foo, string]://a OR [b,]//", - ); - }); - - test("passes through a regex on a tag key containing a colon", () => { - expect(sanitizeQuery("tags[sentry:user]://^id [0-9,]+//")).toBe( - "tags[sentry:user]://^id [0-9,]+//", - ); - }); - - test("passes through a regex on a typed flag key", () => { - expect(sanitizeQuery("flags[beta,string]://^on OR off$//")).toBe( - "flags[beta,string]://^on OR off$//", - ); - }); - - test("passes through a regex on a quoted key", () => { - expect(sanitizeQuery('"my.key"://a OR [b,]//')).toBe( - '"my.key"://a OR [b,]//', - ); - }); - - test("passes through a regex on array keys", () => { - expect( - sanitizeQuery("arr[*]://a OR [b,]// tags[k[*],array]://a OR b//"), - ).toBe("arr[*]://a OR [b,]// tags[k[*],array]://a OR b//"); - }); - - test("passes through a regex right after a closing paren or quote", () => { - expect( - sanitizeQuery('(level:x)message://[a,]// "foo"message://[b,]//'), - ).toBe('(level:x)message://[a,]// "foo"message://[b,]//'); - }); - test("passes through a regex right after an in-list", () => { expect(sanitizeQuery("level:[x,y]message://[a,]//")).toBe( "level:[x,y]message://[a,]//", diff --git a/packages/mcp-core/src/tools/support/search-events/utils.test.ts b/packages/mcp-core/src/tools/support/search-events/utils.test.ts index 38e42ba85..de8fade74 100644 --- a/packages/mcp-core/src/tools/support/search-events/utils.test.ts +++ b/packages/mcp-core/src/tools/support/search-events/utils.test.ts @@ -363,22 +363,10 @@ describe("search query helpers", () => { expect(downgrade).toBe(false); }); - it("should detect regex filters inside parentheses and on Sentry's other key shapes", () => { + // Which key shapes can carry a regex is tested with the grammar in + // packages/toolkit-core/src/search-syntax.test.ts. + it("should detect regex filters and other filters inside parentheses", () => { expect(looksLikeSentrySearchSyntax("(message://a b//)", "logs")).toBe(true); - expect( - looksLikeSentrySearchSyntax("tags[App:Region]://^us-//", "logs"), - ).toBe(true); - expect(looksLikeSentrySearchSyntax("flags[Feature:X]://y//", "logs")).toBe( - true, - ); - expect( - looksLikeSentrySearchSyntax("tags[foo, string]://a b//", "logs"), - ).toBe(true); - expect(looksLikeSentrySearchSyntax('"mykey"://a b//', "logs")).toBe(true); - expect(looksLikeSentrySearchSyntax("arr[*]://a b//", "logs")).toBe(true); - expect(looksLikeSentrySearchSyntax('"Note"message://a b//', "logs")).toBe( - true, - ); // The parser reads filters inside parentheses like any other filter. expect(looksLikeSentrySearchSyntax("(level:error)", "logs")).toBe(true); expect(looksLikeSentrySearchSyntax("(level:error OR level:warning)")).toBe( diff --git a/packages/toolkit-core/src/search-syntax.test.ts b/packages/toolkit-core/src/search-syntax.test.ts index 7df2a1516..57e9295b7 100644 --- a/packages/toolkit-core/src/search-syntax.test.ts +++ b/packages/toolkit-core/src/search-syntax.test.ts @@ -7,6 +7,7 @@ import { getKeyName, type FilterToken, type KeyToken, + type ParseResultToken, parseSearch, parseSearchOrThrow, type SearchConfig, @@ -152,6 +153,112 @@ describe("parseSearch", () => { }); }); +describe("regex filters", () => { + type RegexFilter = { key: string; pattern: string; negated: boolean }; + + function regexFilters(query: string): RegexFilter[] { + const found: RegexFilter[] = []; + const visit = (tokens: ParseResultToken[]): void => { + for (const token of tokens) { + if (token.type === Token.LOGIC_GROUP) { + visit(token.inner); + } else if ( + token.type === Token.FILTER && + token.operator === TermOperator.MATCHES + ) { + found.push({ + key: token.key.text, + pattern: token.value.text, + negated: token.negated, + }); + } + } + }; + visit(parseSearchOrThrow(query, { config: { allowRegex: true } })); + return found; + } + + const regex = (key: string, pattern: string, negated = false) => ({ + key, + pattern, + negated, + }); + + it.each([ + // Pattern contents: commas, brackets, boolean words, parens and quotes + // all stay inside the pattern. + ["message://[a-z,]+//", [regex("message", "[a-z,]+")]], + [ + "message://codes [401,403,) seen//", + [regex("message", "codes [401,403,) seen")], + ], + [ + "message://timeout OR refused//", + [regex("message", "timeout OR refused")], + ], + ["message://read AND write//", [regex("message", "read AND write")]], + [ + "message://(reset|timeout) [0-9,]+//", + [regex("message", "(reset|timeout) [0-9,]+")], + ], + ['message://say "hi" [a,]//', [regex("message", 'say "hi" [a,]')]], + [ + String.raw`!message://^GET OR POST \d+ms//`, + [regex("message", String.raw`^GET OR POST \d+ms`, true)], + ], + // The pattern ends at the first `//` followed by whitespace. + [ + "url://https://x// AND message://a OR b//", + [regex("url", "https://x"), regex("message", "a OR b")], + ], + // Every key shape that can carry a regex. + [ + "tags[foo,string]://[a-z,]+ AND b//", + [regex("tags[foo,string]", "[a-z,]+ AND b")], + ], + [ + "tags[foo, string]://a OR [b,]//", + [regex("tags[foo, string]", "a OR [b,]")], + ], + [ + "tags[sentry:user]://^id [0-9,]+//", + [regex("tags[sentry:user]", "^id [0-9,]+")], + ], + ["tags[App:Region]://^us-//", [regex("tags[App:Region]", "^us-")]], + [ + "flags[beta,string]://^on OR off$//", + [regex("flags[beta,string]", "^on OR off$")], + ], + ["flags[Feature:X]://y//", [regex("flags[Feature:X]", "y")]], + ['"my.key"://a OR [b,]//', [regex('"my.key"', "a OR [b,]")]], + [ + "arr[*]://a OR [b,]// tags[k[*],array]://a OR b//", + [regex("arr[*]", "a OR [b,]"), regex("tags[k[*],array]", "a OR b")], + ], + // Regex filters directly after a paren group or quoted text, and inside + // a paren group. + [ + '(level:x)message://[a,]// "foo"message://[b,]//', + [regex("message", "[a,]"), regex("message", "[b,]")], + ], + ['"Note"message://a b//', [regex("message", "a b")]], + ["(message://a b//)", [regex("message", "a b")]], + ])("parses %j", (query, expected) => { + expect(regexFilters(query)).toEqual(expected); + }); + + it("reads key://pattern// as a literal text filter without allowRegex", () => { + const [filter] = parseSearchOrThrow("message://^Timeout//").filter( + (token): token is FilterToken => token.type === Token.FILTER, + ); + expect(filter).toMatchObject({ + filter: FilterType.TEXT, + operator: TermOperator.DEFAULT, + }); + expect(filter?.value.text).toBe("//^Timeout//"); + }); +}); + describe("getKeyName", () => { function firstKey(query: string): KeyToken { const filter = parseSearchOrThrow(query).find( From eb706b72f9a25f17486b70ba8a5fea809da4744a Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:03:54 +0000 Subject: [PATCH 4/4] fix(cli): Port unscopedCountFilterKeys to the shared parser #1512 landed on main with a call to the old generated parser's parse(), which this branch removes. Use parseSearchNodes, which returns null for queries that don't parse. --- packages/cli/src/lib/search-query.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/lib/search-query.ts b/packages/cli/src/lib/search-query.ts index f76e381a8..8ee7f881b 100644 --- a/packages/cli/src/lib/search-query.ts +++ b/packages/cli/src/lib/search-query.ts @@ -741,15 +741,11 @@ export function unscopedCountFilterKeys(query: string | undefined): string[] { if (!query) { return []; } - let nodes: SearchNode[]; - try { - nodes = parse(normalizeQuery(query)); - } catch (error) { + const nodes = parseSearchNodes(normalizeQuery(query)); + if (!nodes) { // Unparseable query has no keys we can detect; the API will reject it with // a proper 400. Nothing actionable here — just skip the count-scope check. - log.debug( - `Skipping count-scope check for unparseable query: ${String(error)}`, - ); + log.debug("Skipping count-scope check for unparseable query"); return []; } const found = new Set();