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
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

import { parseAst } from 'rollup/parseAst';

import { collectActionCatalogExportFqns } from './action-catalog-fqns';
import { ensureProgram } from './type-guards';

function collect(code: string): Record<string, string> {
const ast = parseAst(code);
const program = ensureProgram(ast, 'fixture.js');
const fqnsByExport = collectActionCatalogExportFqns(program);
return Object.fromEntries(fqnsByExport);
}

describe('Backend Functions - collectActionCatalogExportFqns', () => {
const cases: { description: string; code: string; expected: Record<string, string> }[] = [
{
description: "map each export of the published package's minified module shape",
code: 'import{executeAction as t}from"../../action-execution";function r(e){return t("com.datadoghq.http.getChecksumFromURL",e)}function o(e){return t("com.datadoghq.http.request",e)}export{r as getChecksumFromURL,o as request};',
expected: {
getChecksumFromURL: 'com.datadoghq.http.getChecksumFromURL',
request: 'com.datadoghq.http.request',
},
},
{
description: 'map an exported async function declaration',
code: `
import { getExecuteActionImplementation } from './action-execution.js';
export async function sendSlackMessage(request) {
const implementation = getExecuteActionImplementation();
if (!implementation) {
throw new Error('no implementation');
}
return implementation('com.datadoghq.slack.chat.postMessage', request);
}
`,
expected: { sendSlackMessage: 'com.datadoghq.slack.chat.postMessage' },
},
{
description: 'map exported const arrow and function expressions',
code: `
import { executeAction } from './action-execution.js';
export const listObjects = (e) => executeAction('com.datadoghq.aws.s3.listObjects', e);
const deleteChat = function (e) { return executeAction('com.datadoghq.slack.chat.deleteChat', e); };
export { deleteChat };
`,
expected: {
deleteChat: 'com.datadoghq.slack.chat.deleteChat',
listObjects: 'com.datadoghq.aws.s3.listObjects',
},
},
{
description: 'skip a function calling more than one distinct action',
code: `
export function both(e) {
run('com.datadoghq.http.request', e);
return run('com.datadoghq.slack.chat.postMessage', e);
}
`,
expected: {},
},
{
description: 'skip a function whose first call argument is not an action id',
code: `
export function notAnAction(e) { return run('slack.chat.postMessage', e); }
export function dynamic(name, e) { return run(name, e); }
`,
expected: {},
},
{
description: 'skip re-exports, default exports and non-function exports',
code: `
export { request } from './http.js';
export * from './slack.js';
export default function (e) { return run('com.datadoghq.http.request', e); }
export const FQN = 'com.datadoghq.http.request';
let mutable = (e) => run('com.datadoghq.http.request', e);
export { mutable };
`,
expected: {},
},
];

test.each(cases)('Should $description', ({ code, expected }) => {
const fqnsByExport = collect(code);
expect(fqnsByExport).toEqual(expected);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

import type { Node, Program } from 'estree';

import { isStringLiteral } from './type-guards';
import { walkAst } from './walk-ast';

export const ACTION_FQN_PREFIX = 'com.datadoghq.';

type FunctionNode = Extract<
Node,
{ type: 'FunctionDeclaration' | 'FunctionExpression' | 'ArrowFunctionExpression' }
>;

/**
* Maps each exported function of an `@datadog/action-catalog` module to the one `com.datadoghq.*`
* action FQN it calls the executor with. The import path alone doesn't name the action:
* `aws/agent` exports `com.datadoghq.aws.bedrock.agent.*`.
*/
export function collectActionCatalogExportFqns(ast: Program): Map<string, string> {
const functionsByName = collectTopLevelFunctions(ast);
const fqnsByExport = new Map<string, string>();
const addExport = (exportedName: string, fn: FunctionNode | undefined) => {
const fqn = fn && getSingleActionFqn(fn);
if (fqn) {
fqnsByExport.set(exportedName, fqn);
}
};

for (const node of ast.body) {
if (node.type !== 'ExportNamedDeclaration' || node.source) {
continue;
}
if (node.declaration?.type === 'FunctionDeclaration' && node.declaration.id) {
addExport(node.declaration.id.name, node.declaration);
}
if (node.declaration?.type === 'VariableDeclaration') {
for (const declarator of node.declaration.declarations) {
if (declarator.id.type === 'Identifier') {
const fn = functionsByName.get(declarator.id.name);
addExport(declarator.id.name, fn);
}
}
}
for (const specifier of node.specifiers) {
if (specifier.local.type !== 'Identifier' || specifier.exported.type !== 'Identifier') {
continue;
}
const fn = functionsByName.get(specifier.local.name);
addExport(specifier.exported.name, fn);
}
}

return fqnsByExport;
}

function collectTopLevelFunctions(ast: Program): Map<string, FunctionNode> {
const functionsByName = new Map<string, FunctionNode>();
for (const statement of ast.body) {
const node =
statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement;
if (node?.type === 'FunctionDeclaration' && node.id) {
functionsByName.set(node.id.name, node);
}
if (node?.type !== 'VariableDeclaration' || node.kind !== 'const') {
continue;
}
for (const declarator of node.declarations) {
const init = declarator.init;
if (
declarator.id.type === 'Identifier' &&
(init?.type === 'ArrowFunctionExpression' || init?.type === 'FunctionExpression')
) {
functionsByName.set(declarator.id.name, init);
}
}
}
return functionsByName;
}

function getSingleActionFqn(fn: FunctionNode): string | undefined {
const fqns = new Set<string>();
walkAst(fn.body, fqns, {
CallExpression(call, { state }) {
const [firstArg] = call.arguments;
if (isStringLiteral(firstArg) && firstArg.value.startsWith(ACTION_FQN_PREFIX)) {
state.add(firstArg.value);
}
},
});
return fqns.size === 1 ? [...fqns][0] : undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { Program } from 'estree';

import { isTypeOnly } from './type-guards';

const ACTION_CATALOG_PACKAGE = '@datadog/action-catalog';
export const ACTION_CATALOG_PACKAGE = '@datadog/action-catalog';

export interface ActionCatalogImports {
functions: Set<string>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ const OPTION_HOOK_KEYS: ReadonlySet<string> = new Set(['__proto__', 'toJSON', 't

type ConnectionIdProperty = Property & { value: Expression };

export interface ActionCatalogCall {
record: ParsedModuleRecord;
callSite: SimpleCallExpression;
connectionId: string | undefined;
}

/**
* Extracts the conservative backend-file connection ID union from module records
* collected while the backend bundler walked the real execution graph.
Expand All @@ -33,7 +39,30 @@ export function extractConnectionIdsFromModuleGraph(
modules: ReadonlyMap<string, ParsedModuleRecord>,
buildRoot: string,
): string[] {
const calls = collectActionCatalogCalls(entryId, modules, buildRoot);
return getConnectionIds(calls);
}

export function getConnectionIds(calls: ActionCatalogCall[]): string[] {
const connectionIds = new Set<string>();

for (const { connectionId } of calls) {
// `''` is a valid declared connectionId, not "none" — only the absence of a
// `connectionId` property (`undefined`) means no restriction to record.
if (connectionId !== undefined) {
connectionIds.add(connectionId);
}
}

return [...connectionIds].sort();
}

export function collectActionCatalogCalls(
entryId: string,
modules: ReadonlyMap<string, ParsedModuleRecord>,
buildRoot: string,
): ActionCatalogCall[] {
const calls: ActionCatalogCall[] = [];
let mutatedImportedObjectVariables: Set<eslintScope.Variable> | undefined;
const getMutatedImportedObjectVariables = () => {
mutatedImportedObjectVariables ??= collectMutatedImportedObjectVariables(modules);
Expand All @@ -54,15 +83,11 @@ export function extractConnectionIdsFromModuleGraph(
record,
getMutatedImportedObjectVariables,
);
// `''` is a valid declared connectionId, not "none" — only the absence of a
// `connectionId` property (`undefined`) means no restriction to record.
if (connectionId !== undefined) {
connectionIds.add(connectionId);
}
calls.push({ record, callSite, connectionId });
}
});

return [...connectionIds].sort();
return calls;
}

function extractConnectionIdFromActionCall(
Expand Down
Loading
Loading