Repository navigation
ref(cli): Parse every kind of ID argument with one grammar #1714
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+2,719
−1,549
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
07f92a3
ref(cli): Parse arity-dependent arguments in the scanner
sentry-junior[bot] 1fa4eff
fix(cli): Skip a subcommand name before parsing bound arguments
sentry-junior[bot] 5bc364c
ref(cli): Parse bound arguments without patching Stricli
sentry-junior[bot] c035d23
ref(cli): Parse ID arguments with a Peggy grammar
sentry-junior[bot] 6bb2a8a
fix(cli): Keep a path's trace ID, and reject empty path segments
sentry-junior[bot] e524d64
style(cli): Format id-arguments tests
sentry-junior[bot] 0a4ad5a
fix(cli): Parse a replay ID after an empty target
sentry-junior[bot] 11d9eb0
ref(cli): Parse every kind of ID argument with one grammar
sentry-junior[bot] 076b3a1
fix(cli): Recover a malformed ID after the org
sentry-junior[bot] 9a899d4
ref(cli): Parse trace, span and log view arguments in the scanner
sentry-junior[bot] 099a650
fix(cli): Recover a prefixed ID after the org
sentry-junior[bot] 07775e4
fix(cli): Look up a prefixed partial ID instead of calling it a slug
sentry-junior[bot] b5efd67
test(cli): Keep partial-ID-shaped slugs out of the log view target pr…
sentry-junior[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| // Types of the parser generated from id-arguments.pegjs. The generator | ||
| // copies this file to src/generated/id-arguments-grammar.d.ts and rewrites | ||
| // relative imports to resolve from there. | ||
|
|
||
| /** An ID a Sentry URL carries, with the URL's org and server. */ | ||
| export type IdUrl = { | ||
| readonly id: string; | ||
| readonly org?: string; | ||
| readonly baseUrl?: string; | ||
| }; | ||
|
|
||
| /** | ||
| * What the command parses: the grammar itself does not know any kind of ID. | ||
| * See `parseIdArguments()`. | ||
| */ | ||
| export type IdArgumentsGrammarOptions = { | ||
| /** Whether the arguments may name no ID (then a lone token is a target). */ | ||
| optional: boolean; | ||
| /** Whether `value` is an ID of the kind the command takes. */ | ||
| isId: (value: string) => boolean; | ||
| /** Whether `value`, not an ID, still looks like a malformed one. */ | ||
| mayBeId: (value: string) => boolean; | ||
| /** The ID in a Sentry URL, or `null` for anything else. */ | ||
| idUrl: (value: string) => IdUrl | null; | ||
| }; | ||
|
|
||
| /** A token that names an ID, maybe after a target: `[<target>/]<id>`. */ | ||
| export type IdToken = { | ||
| /** The token as typed */ | ||
| readonly text: string; | ||
| /** What came before the ID's slash, if anything */ | ||
| readonly target?: string; | ||
| /** The ID, not validated */ | ||
| readonly id: string; | ||
| }; | ||
|
|
||
| /** Which tokens are the target and which are IDs. */ | ||
| export type IdArgumentsSyntax = { | ||
| /** The target, as typed: a separate argument, a path prefix, or `<org>/` */ | ||
| readonly target?: string; | ||
| /** Whether `target` came before the first ID in one path */ | ||
| readonly path?: boolean; | ||
| readonly ids: readonly IdToken[]; | ||
| /** Server of a pasted Sentry URL */ | ||
| readonly baseUrl?: string; | ||
| /** Whether the ID came before the target */ | ||
| readonly reversed?: boolean; | ||
| }; | ||
|
|
||
| export declare class SyntaxError extends Error { | ||
| expected: unknown; | ||
| found: string | null; | ||
| location: unknown; | ||
| } | ||
|
|
||
| /** Parse NUL-prefixed tokens into a target and IDs. */ | ||
| export declare function parse( | ||
| input: string, | ||
| options: IdArgumentsGrammarOptions, | ||
| ): IdArgumentsSyntax; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| // Grammar for positionals that name IDs and the org/project to look them up | ||
| // in. It knows nothing about what kind of ID it parses (trace, span, replay, | ||
| // log...): the command says which, through the `isId` and `idUrl` hooks in | ||
| // `options`, so every ID-taking command shares one syntax: | ||
| // | ||
| // <id> the org/project is auto-detected | ||
| // <org>/<id> like a URL: the org, then the ID (the | ||
| // caller may read `<org>` as a project) | ||
| // <org>/<project>/<id> | ||
| // <target> <id> any org/project target, as everywhere else | ||
| // <id> <org>/<project> reversed; the caller warns | ||
| // <id-url> a Sentry URL with the ID in it | ||
| // | ||
| // More IDs may follow any of these, each repeating the target or not | ||
| // (`<target>/<id>`). The ID is always the last path segment, and slashes | ||
| // pasted after it are ignored. | ||
| // | ||
| // `pnpm run generate:grammar` compiles it, with the types in | ||
| // id-arguments.d.ts, into the gitignored src/generated/; the build, | ||
| // typecheck, and test scripts run it. | ||
| // | ||
| // `parseIdArguments()` (src/lib/id-arguments.ts) splits the arguments into | ||
| // tokens first and puts a NUL, which no argument contains, before each one. | ||
| // No token is empty. The grammar only decides which token is the target and | ||
| // which are IDs; it accepts any input and never throws. The caller counts | ||
| // the IDs, validates them, and parses the target like any org/project target. | ||
|
|
||
| { | ||
| const { optional, isId, mayBeId, idUrl } = options; | ||
|
|
||
| // `[<org>/[<project>/]]` before an ID; the caller makes it a target | ||
| function pathTarget(prefix) { | ||
| if (prefix === "") { | ||
| return {}; | ||
| } | ||
| return { target: prefix, path: true }; | ||
| } | ||
| } | ||
|
|
||
| // Peggy runs a rule's action as soon as that rule matches, even when the | ||
| // enclosing alternative fails later and the parser backtracks. Every action | ||
| // here only builds values, and the hooks are pure, so that is harmless. | ||
|
|
||
| arguments | ||
| = end { return { ids: [] }; } | ||
| // A Sentry URL with the ID in it | ||
| / sep u:urlWithId x:more { | ||
| return { | ||
| ...(u.org && { target: `${u.org}/` }), | ||
| ids: [{ text: u.id, id: u.id }, ...x], | ||
| ...(u.baseUrl && { baseUrl: u.baseUrl }), | ||
| }; | ||
| } | ||
| // Any other URL is a target | ||
| / sep t:url x:more { return { target: t, ids: x }; } | ||
| // `<id> <org>/<project>`: the arguments are reversed. When IDs are | ||
| // optional, only a real ID can come first. | ||
| / sep i:segment &{ return !optional || isId(i); } sep !idPath | ||
| t:$([^/\0]* "/" [^\0]*) end | ||
| { return { target: t, ids: [{ text: i, id: i }], reversed: true }; } | ||
| // `[<org>/[<project>/]]<id> [<id>...]` | ||
| / sep p:idPath x:more { | ||
| return { | ||
| ...pathTarget(p.target ?? ""), | ||
| ids: [{ text: p.text, id: p.id }, ...x], | ||
| }; | ||
| } | ||
| // One token that does not end in an ID. Unless IDs are optional, it still | ||
| // names one, so it can be reported or recovered: the last segment after | ||
| // two slashes (`<org>/<project>/<id>`), after one when it looks like a | ||
| // malformed ID (`<org>/<partial-id>`), or the whole token. Otherwise, | ||
| // `<org>/<project>` is a target without an ID. | ||
| / sep t:segment "/" i:segment end &{ return !optional && mayBeId(i); } | ||
| { return { ...pathTarget(t), ids: [{ text: text().slice(1), id: i }] }; } | ||
| / sep !"/" t:$(segment "/" segment "/" [^\0]*) end &{ return !optional; } { | ||
| const slash = t.lastIndexOf("/"); | ||
| return { | ||
| ...pathTarget(t.slice(0, slash)), | ||
| ids: [{ text: t, id: t.slice(slash + 1) }], | ||
| }; | ||
| } | ||
| / sep i:segment end &{ return !optional; } { return { ids: [{ text: i, id: i }] }; } | ||
| // `<target> [<id>...]` | ||
| / sep t:token x:more { return { target: t, ids: x }; } | ||
|
|
||
| // The IDs after the first token. Each may repeat the target: | ||
| // `<target>/<id>`. | ||
| more | ||
| = (sep @(idPath / t:token { return { text: t, id: t }; }))* | ||
|
|
||
| // `[<target>/]<id>` in one token: the last segment is an ID, maybe with | ||
| // slashes pasted after it. | ||
| idPath | ||
| = t:$(!("/" idSegment argEnd) [^\0])* "/" i:idSegment argEnd | ||
| { return { text: text(), target: t, id: i }; } | ||
| / i:idSegment argEnd { return { text: text(), id: i }; } | ||
|
|
||
| // An ID, and the slashes after it | ||
| idSegment | ||
| = i:segment "/"* &{ return isId(i); } { return i; } | ||
|
|
||
| // A Sentry URL carrying the ID | ||
| urlWithId | ||
| = u:token &{ return idUrl(u) !== null; } { return idUrl(u); } | ||
|
|
||
| url | ||
| = $("http" "s"? "://" [^\0]*) | ||
|
|
||
| token = $[^\0]+ | ||
|
|
||
| segment = $[^/\0]+ | ||
|
|
||
| sep = "\0" | ||
|
|
||
| argEnd = &sep / end | ||
|
|
||
| end = !. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.