Skip to content
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

[Query API] Location Query #1046

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
12 changes: 12 additions & 0 deletions src/cli/repl/commands/repl-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { NodeId } from '../../../r-bridge/lang-4.x/ast/model/processing/nod
import { BuiltIn } from '../../../dataflow/environments/built-in';
import { graphToMermaidUrl } from '../../../util/mermaid/dfg';
import { normalizedAstToMermaidUrl } from '../../../util/mermaid/ast';
import { labeledSourceRangeToString } from '../../../queries/catalog/location-query/location-query-format';

async function getDataflow(shell: RShell, remainingLine: string) {
return await new PipelineExecutor(DEFAULT_DATAFLOW_PIPELINE, {
Expand Down Expand Up @@ -133,6 +134,17 @@ export function asciiSummaryOfQueryResult(formatter: OutputFormatter, totalInMs:
result.push(`Query: ${bold(query, formatter)} (${out['.meta'].timing.toFixed(0)}ms)`);
result.push(` ╰ [Dataflow Graph](${graphToMermaidUrl(out.graph)})`);
continue;
} else if(query === 'location') {
const out = queryResults as QueryResults<'location'>['location'];
result.push(`Query: ${bold(query, formatter)} (${out['.meta'].timing.toFixed(0)}ms)`);
result.push(' ├ Location');
const locationArray = Object.entries(out.location);
for(let i = 0; i < locationArray.length - 1; i++) {
const [nodeId, location] = locationArray[i];
result.push(` ├ ${nodeId} at ${location ? `${labeledSourceRangeToString(location)}` : 'unknown'}`);
}
const [nodeId, location] = locationArray[locationArray.length - 1];
result.push(` ╰ ${nodeId} at ${location ? `${labeledSourceRangeToString(location)}` : 'unknown'}`);
} else if(query === 'id-map') {
const out = queryResults as QueryResults<'id-map'>['id-map'];
result.push(`Query: ${bold(query, formatter)} (${out['.meta'].timing.toFixed(0)}ms)`);
Expand Down
22 changes: 22 additions & 0 deletions src/documentation/print-query-wiki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { exampleQueryCode } from './data/query/example-query-code';
import { details } from './doc-util/doc-structure';
import { codeBlock } from './doc-util/doc-code';
import { executeDataflowQuery } from '../queries/catalog/dataflow-query/dataflow-query-executor';
import { executeLocationQuery } from '../queries/catalog/location-query/location-query-executor';
import { executeIdMapQuery } from '../queries/catalog/id-map-query/id-map-query-executor';
import { executeNormalizedAstQuery } from '../queries/catalog/normalized-ast-query/normalized-ast-query-executor';

Expand Down Expand Up @@ -148,6 +149,27 @@ ${
}
});

registerQueryDocumentation('location', {
name: 'Location Query',
type: 'active',
shortDescription: 'Returns the location of a specific node.',
functionName: executeLocationQuery.name,
functionFile: '../queries/catalog/location-query/location-query-executor.ts',
buildExplanation: async(shell: RShell) => {
return `
If you are interested in the location of a specific node, you can use the location query.

Using the example code from above, the following query returns the location of the first occurrence of the symbol \`m\`:
${
await showQuery(shell, exampleQueryCode, [{
type: 'location',
nodeId: '24'
}], { showCode: true })
}
`;
}
});

registerQueryDocumentation('compound', {
name: 'Compound Query',
type: 'virtual',
Expand Down
17 changes: 17 additions & 0 deletions src/queries/catalog/location-query/location-query-executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { BasicQueryData } from '../../query';
import type { LocationQuery, LocationQueryResult } from './location-query-format';
import { labelSourceRange } from './location-query-format';


export function executeLocationQuery({ ast }: BasicQueryData, queries: readonly LocationQuery[]): LocationQueryResult {
return {
'.meta': {
/* there is no sense in measuring a get */
timing: 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given there are multiple hashmap access operations, i would in-fact measure the time, as we are no longer constant!

},
location: Object.fromEntries(queries.map(({ nodeId }) => {
const location = ast.idMap.get(nodeId)?.location;
return [nodeId, location && labelSourceRange(location)];
}))
};
}
27 changes: 27 additions & 0 deletions src/queries/catalog/location-query/location-query-format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { BaseQueryFormat, BaseQueryResult } from '../../base-query-format';
import type { SourceRange } from '../../../util/range';
import type { NodeId } from '../../../r-bridge/lang-4.x/ast/model/processing/node-id';

export interface LabeledSourceRange {
readonly startLine: number;
readonly startColumn: number;
readonly endLine: number;
readonly endColumn: number;
}

export function labelSourceRange([startLine, startColumn, endLine, endColumn]: SourceRange): LabeledSourceRange {
return { startLine, startColumn, endLine, endColumn };
}

export function labeledSourceRangeToString({ startLine, startColumn, endLine, endColumn }: LabeledSourceRange): string {
return `${startLine}:${startColumn} – ${endLine}:${endColumn}`;
}

export interface LocationQuery extends BaseQueryFormat {
readonly type: 'location';
readonly nodeId: NodeId;
}

export interface LocationQueryResult extends BaseQueryResult {
readonly location: Record<NodeId, LabeledSourceRange | undefined>;
}
6 changes: 6 additions & 0 deletions src/queries/query-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export const DataflowQuerySchema = Joi.object({
type: Joi.string().valid('dataflow').required().description('The type of the query.'),
}).description('The dataflow query simply returns the dataflow graph, there is no need to pass it multiple times!');

export const LocationQuerySchema = Joi.object({
type: Joi.string().valid('location').required().description('The type of the query.'),
nodeId: Joi.string().required().description('The node id to get the location of.')
}).description('Location query used to find the location of a specific node');

export const IdMapQuerySchema = Joi.object({
type: Joi.string().valid('id-map').required().description('The type of the query.'),
}).description('The id map query retrieves the id map from the normalized AST.');
Expand All @@ -29,6 +34,7 @@ export const NormalizedAstQuerySchema = Joi.object({
export const SupportedQueriesSchema = Joi.alternatives(
CallContextQuerySchema,
DataflowQuerySchema,
LocationQuerySchema,
IdMapQuerySchema,
NormalizedAstQuerySchema
).description('Supported queries');
Expand Down
5 changes: 4 additions & 1 deletion src/queries/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import type { VirtualCompoundConstraint } from './virtual-query/compound-query';
import type { NormalizedAst } from '../r-bridge/lang-4.x/ast/model/processing/decorate';
import { executeDataflowQuery } from './catalog/dataflow-query/dataflow-query-executor';
import type { DataflowQuery } from './catalog/dataflow-query/dataflow-query-format';
import type { LocationQuery } from './catalog/location-query/location-query-format';
import { executeLocationQuery } from './catalog/location-query/location-query-executor';
import { executeIdMapQuery } from './catalog/id-map-query/id-map-query-executor';
import type { IdMapQuery } from './catalog/id-map-query/id-map-query-format';
import { executeNormalizedAstQuery } from './catalog/normalized-ast-query/normalized-ast-query-executor';
import type { NormalizedAstQuery } from './catalog/normalized-ast-query/normalized-ast-query-format';

export type Query = CallContextQuery | DataflowQuery | NormalizedAstQuery | IdMapQuery;
export type Query = CallContextQuery | DataflowQuery | NormalizedAstQuery | IdMapQuery | LocationQuery;

export type QueryArgumentsWithType<QueryType extends BaseQueryFormat['type']> = Query & { type: QueryType };

Expand All @@ -34,6 +36,7 @@ type SupportedQueries = {
export const SupportedQueries = {
'call-context': executeCallContextQueries,
'dataflow': executeDataflowQuery,
'location': executeLocationQuery,
'id-map': executeIdMapQuery,
'normalized-ast': executeNormalizedAstQuery
} as const satisfies SupportedQueries;
Expand Down
33 changes: 33 additions & 0 deletions test/functionality/dataflow/query/location-query-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { assertQuery } from '../../_helper/query';
import { label } from '../../_helper/label';
import { withShell } from '../../_helper/shell';
import type {
LabeledSourceRange,
LocationQuery
} from '../../../../src/queries/catalog/location-query/location-query-format';



describe('Location Query', withShell(shell => {
function testQuery(name: string, code: string, query: readonly LocationQuery[], expected: Record<string, LabeledSourceRange>) {
assertQuery(label(name), shell, code, query, { location: { location: expected } });
}

testQuery('Single location query', 'x + 1', [{ type: 'location', nodeId: 0 }], { 0: {
startLine: 1,
startColumn: 1,
endLine: 1,
endColumn: 1
} });
testQuery('Multiple Queries', 'x + 1\ny + 2', [{ type: 'location', nodeId: 0 }, { type: 'location', nodeId: 3 }], { 0: {
startLine: 1,
startColumn: 1,
endLine: 1,
endColumn: 1
}, 3: {
startLine: 2,
startColumn: 1,
endLine: 2,
endColumn: 1
} });
}));