Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions packages/web/src/features/search/parser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, it, vi } from 'vitest';
import type { PrismaClient } from '@sourcebot/db';
import { parseQuerySyntaxIntoIR } from './parser';

describe('parseQuerySyntaxIntoIR', () => {
it('resolves anchored repo display names to repo_set queries', async () => {
const findMany = vi.fn().mockResolvedValue([
{ name: 'gerrit.example.com:29418/zximgw/rcsiap2001' },
]);

const prisma = {
repo: {
findMany,
},
} as unknown as PrismaClient;

const query = await parseQuerySyntaxIntoIR({
query: 'repo:"^zximgw/rcsiap2001$"',
options: {},
prisma,
});

expect(findMany).toHaveBeenCalledWith({
where: {
orgId: expect.any(Number),
OR: [
{ name: 'zximgw/rcsiap2001' },
{ displayName: 'zximgw/rcsiap2001' },
],
},
select: { name: true },
});

expect(query.repo_set).toBeDefined();
expect(query.repo_set?.set).toEqual({
'gerrit.example.com:29418/zximgw/rcsiap2001': true,
});
});

it('falls back to regex handling when pattern is not a literal string', async () => {
const findMany = vi.fn();
const prisma = {
repo: {
findMany,
},
} as unknown as PrismaClient;

const query = await parseQuerySyntaxIntoIR({
query: 'repo:^gerrit.*$',
options: {},
prisma,
});

expect(findMany).not.toHaveBeenCalled();
expect(query.repo?.regexp).toEqual('^gerrit.*$');
});
});
76 changes: 76 additions & 0 deletions packages/web/src/features/search/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { SINGLE_TENANT_ORG_ID } from '@/lib/constants';
import { ServiceErrorException } from '@/lib/serviceError';
import { StatusCodes } from 'http-status-codes';
import { ErrorCode } from '@/lib/errorCodes';
import escapeStringRegexp from 'escape-string-regexp';

// Configure the parser to throw errors when encountering invalid syntax.
const parser = _parser.configure({
Expand Down Expand Up @@ -95,6 +96,26 @@ export const parseQuerySyntaxIntoIR = async ({

return context.repos.map((repo) => repo.name);
},
onResolveRepoExactMatch: async (literalRepoName: string) => {
const repos = await prisma.repo.findMany({
where: {
orgId: SINGLE_TENANT_ORG_ID,
OR: [
{ name: literalRepoName },
{ displayName: literalRepoName },
],
},
select: {
name: true,
}
});

if (repos.length === 0) {
return undefined;
}

return repos.map((repo) => repo.name);
},
});
} catch (error) {
if (error instanceof SyntaxError) {
Expand All @@ -117,12 +138,14 @@ const transformTreeToIR = async ({
isCaseSensitivityEnabled,
isRegexEnabled,
onExpandSearchContext,
onResolveRepoExactMatch,
}: {
tree: Tree;
input: string;
isCaseSensitivityEnabled: boolean;
isRegexEnabled: boolean;
onExpandSearchContext: (contextName: string) => Promise<string[]>;
onResolveRepoExactMatch?: (literalRepoName: string) => Promise<string[] | undefined>;
}): Promise<QueryIR> => {
const transformNode = async (node: SyntaxNode): Promise<QueryIR> => {
switch (node.type.id) {
Expand Down Expand Up @@ -239,6 +262,16 @@ const transformTreeToIR = async ({
};

case RepoExpr:
if (onResolveRepoExactMatch) {
const repoSet = await resolveRepoLiteralIfPossible({
value,
onResolveRepoExactMatch,
});
if (repoSet) {
return repoSet;
}
}

return {
repo: {
regexp: value
Expand Down Expand Up @@ -409,3 +442,46 @@ const getChildren = (node: SyntaxNode): SyntaxNode[] => {
}
return children;
}

const resolveRepoLiteralIfPossible = async ({
value,
onResolveRepoExactMatch,
}: {
value: string;
onResolveRepoExactMatch: (literalRepoName: string) => Promise<string[] | undefined>;
}): Promise<QueryIR | undefined> => {
const literalMatch = value.match(/^\^(.*)\$/);
if (!literalMatch) {
return undefined;
}

const innerPattern = literalMatch[1];
const unescaped = unescapeRegexLiteral(innerPattern);

if (escapeStringRegexp(unescaped) !== innerPattern) {
return undefined;
}

const repoNames = await onResolveRepoExactMatch(unescaped);
if (!repoNames || repoNames.length === 0) {
return undefined;
}

return {
repo_set: {
set: repoNames.reduce((acc, name) => {
acc[name.trim()] = true;
return acc;
}, {} as Record<string, boolean>)
},
query: "repo_set"
};
}

const unescapeRegexLiteral = (pattern: string) => {
const hexUnescaped = pattern.replace(/\\x([0-9a-fA-F]{2})/g, (_match, hex) => {
return String.fromCharCode(parseInt(hex, 16));
});

return hexUnescaped.replace(/\\([\\.^$|?*+()[\]{}])/g, (_match, char) => char);
}