mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
fix(graphs): separate source identities from display representations
Validation: integrated changes passed formatting, typecheck, lint, resolved module-boundary verification and git diff --check. Tests and real-provider workflows were not run.
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
} from "./JavaScriptArtifactGraphContext.js";
|
||||
import { astObservationEvidence } from "./JavaScriptArtifactGraphEvidence.js";
|
||||
import { compositeKey } from "../../domain/unicodeCodePointOrder.js";
|
||||
import { javascriptDisplayText } from "../../domain/javascript/javascriptAstValues.js";
|
||||
import {
|
||||
addElectronInferenceEdge,
|
||||
electronFindingSourceNode,
|
||||
@@ -51,17 +52,27 @@ const addNativeBinding = (input: NativeBindingInput): void => {
|
||||
identity: electronObservationIdentity(
|
||||
context,
|
||||
"javascript-requested-native-export",
|
||||
// Members are a variable-length source-derived list: compositeKey keeps
|
||||
// ["a\0b"] distinct from ["a", "b"] inside the digested identity.
|
||||
`${file.path}:${value.specifier}:${value.binding_kind}:${compositeKey(value.members)}:${electronRangeKey(value.location)}`,
|
||||
compositeKey([
|
||||
file.path,
|
||||
value.specifier,
|
||||
value.binding_kind,
|
||||
value.members,
|
||||
value.namespace_access ?? null,
|
||||
value.dynamic_member_access ?? null,
|
||||
electronRangeKey(value.location),
|
||||
]),
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label: value.members.join(", "),
|
||||
label:
|
||||
value.members.map(javascriptDisplayText).join(", ") ||
|
||||
value.specifier,
|
||||
properties: {
|
||||
specifier: value.specifier,
|
||||
binding_kind: value.binding_kind,
|
||||
requested_members: value.members,
|
||||
namespace_access: value.namespace_access ?? null,
|
||||
dynamic_member_access: value.dynamic_member_access ?? null,
|
||||
resolved_path: resolved,
|
||||
native_export_verification: "not-performed",
|
||||
},
|
||||
@@ -90,6 +101,8 @@ const addNativeBinding = (input: NativeBindingInput): void => {
|
||||
specifier: value.specifier,
|
||||
binding_kind: value.binding_kind,
|
||||
requested_members: value.members,
|
||||
namespace_access: value.namespace_access ?? null,
|
||||
dynamic_member_access: value.dynamic_member_access ?? null,
|
||||
},
|
||||
limitations: [
|
||||
"Requested JavaScript members are not verified native exports.",
|
||||
@@ -107,6 +120,8 @@ const addNativeBinding = (input: NativeBindingInput): void => {
|
||||
properties: {
|
||||
resolved_path: resolved,
|
||||
requested_members: value.members,
|
||||
namespace_access: value.namespace_access ?? null,
|
||||
dynamic_member_access: value.dynamic_member_access ?? null,
|
||||
verified: false,
|
||||
},
|
||||
confidence: "medium",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
|
||||
import type { JavaScriptBundlerRegistration } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import { compositeKey } from "../../domain/unicodeCodePointOrder.js";
|
||||
import {
|
||||
addAstContainsEdge,
|
||||
artifactLocalIdentity,
|
||||
@@ -80,7 +81,11 @@ const createBundlerChunkNode = (
|
||||
const chunkKey = `${registration.runtime}:${registration.chunk_keys.join(",")}`;
|
||||
return context.accumulator.addNode({
|
||||
kind: "javascript-chunk",
|
||||
identity: artifactLocalIdentity(file.sha256, "bundler-chunk", chunkKey),
|
||||
identity: artifactLocalIdentity(
|
||||
file.sha256,
|
||||
"bundler-chunk",
|
||||
compositeKey([registration.runtime, registration.chunk_keys]),
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label: chunkKey,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
|
||||
import { compositeKey } from "../../domain/unicodeCodePointOrder.js";
|
||||
import {
|
||||
completeApplicationCoverage,
|
||||
partialApplicationCoverage,
|
||||
@@ -330,7 +331,7 @@ export const chunkLookupKey = (
|
||||
path: string,
|
||||
runtime: string,
|
||||
chunkKey: string,
|
||||
): string => `${path}\0${runtime}\0${chunkKey}`;
|
||||
): string => compositeKey([path, runtime, chunkKey]);
|
||||
|
||||
/** Resolve a relative static specifier without escaping the artifact root. */
|
||||
export const resolveArtifactPath = (
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { posix } from "node:path";
|
||||
|
||||
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
||||
import {
|
||||
compareUnicodeCodePoints,
|
||||
compositeKey,
|
||||
} from "../../domain/unicodeCodePointOrder.js";
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
|
||||
import {
|
||||
completeApplicationCoverage,
|
||||
@@ -177,7 +180,7 @@ const addExportRelationship = (
|
||||
identity: artifactLocalIdentity(
|
||||
file.sha256,
|
||||
"module-export",
|
||||
`${file.container_sha256}:${file.path}:${link.exportedName}`,
|
||||
compositeKey([file.container_sha256, file.path, link.exportedName]),
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
|
||||
@@ -66,7 +66,9 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
let omittedHierarchyReferences = 0;
|
||||
for (const document of input.documents.slice(0, input.limits.max_documents)) {
|
||||
const parsed = parseInterfaceBuilderRecords(document.raw);
|
||||
const prefix = `ib:${document.relativePath}:`;
|
||||
const prefix = `ib:${JSON.stringify(document.relativePath)}:`;
|
||||
const graphId = (kind: string, parts: readonly unknown[] = []): string =>
|
||||
`${prefix}${kind}:${JSON.stringify(parts)}`;
|
||||
const rootId = `${prefix}root`;
|
||||
const objectIds = new Set(parsed.objects.map(({ id }) => id));
|
||||
const evidenceFor = (description: string) => [
|
||||
@@ -104,7 +106,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
addNode(rootNode);
|
||||
let documentObjectCount = 0;
|
||||
for (const object of parsed.objects.slice(0, input.limits.max_objects)) {
|
||||
const id = `${prefix}object:${object.id}`;
|
||||
const id = graphId("object", [object.id]);
|
||||
const kind = object.kind === "other" ? "unknown" : object.kind;
|
||||
if (
|
||||
!addNode({
|
||||
@@ -129,7 +131,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
let documentOmittedConnections = 0;
|
||||
let documentOmittedHierarchyEdges = 0;
|
||||
const objectNodeId = (objectId: string): string =>
|
||||
`${prefix}object:${objectId}`;
|
||||
graphId("object", [objectId]);
|
||||
const addEdge = (
|
||||
edge: (typeof edges)[number],
|
||||
isHierarchy: boolean,
|
||||
@@ -238,7 +240,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
const known = objectIds.has(objectId);
|
||||
const childId = known
|
||||
? objectNodeId(objectId)
|
||||
: `${prefix}unknown:${objectId}`;
|
||||
: graphId("unknown", [objectId]);
|
||||
if (
|
||||
!known &&
|
||||
!addNode({
|
||||
@@ -252,7 +254,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
)
|
||||
hierarchyTruncated = true;
|
||||
connect(
|
||||
`${prefix}hierarchy:${parentId}:${childId}`,
|
||||
graphId("hierarchy", [parentId, childId]),
|
||||
parentId,
|
||||
childId,
|
||||
"contains",
|
||||
@@ -281,11 +283,11 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
const destinationId =
|
||||
item.destination_id === null ? null : objectNodeId(item.destination_id);
|
||||
if (item.kind === "action") {
|
||||
const actionId = `${prefix}action:${item.id}`;
|
||||
const actionId = graphId("action", [item.id]);
|
||||
const selectorId =
|
||||
item.label === null
|
||||
? null
|
||||
: `${prefix}selector:${item.destination_id ?? "unknown"}:${item.label}`;
|
||||
: graphId("selector", [item.destination_id, item.label]);
|
||||
addNode({
|
||||
id: actionId,
|
||||
kind: "action",
|
||||
@@ -299,7 +301,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
evidence: evidenceFor(`action ${item.id}`),
|
||||
});
|
||||
connect(
|
||||
`${actionId}:source`,
|
||||
graphId("action-edge", [item.id, "source"]),
|
||||
sourceId,
|
||||
actionId,
|
||||
"target_action",
|
||||
@@ -315,7 +317,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
evidence: evidenceFor(`selector ${item.label}`),
|
||||
});
|
||||
connect(
|
||||
`${actionId}:selector`,
|
||||
graphId("action-edge", [item.id, "selector"]),
|
||||
actionId,
|
||||
selectorId,
|
||||
"target_action",
|
||||
@@ -324,7 +326,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
}
|
||||
if (destinationId === null || !nodes.has(destinationId))
|
||||
connect(
|
||||
`${actionId}:destination`,
|
||||
graphId("action-edge", [item.id, "destination"]),
|
||||
actionId,
|
||||
null,
|
||||
"target_action",
|
||||
@@ -333,14 +335,14 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
);
|
||||
else
|
||||
connect(
|
||||
`${actionId}:destination`,
|
||||
graphId("action-edge", [item.id, "destination"]),
|
||||
actionId,
|
||||
destinationId,
|
||||
"target_action",
|
||||
`action destination ${item.destination_id}`,
|
||||
);
|
||||
} else if (item.kind === "outlet") {
|
||||
const outletId = `${prefix}outlet:${item.id}`;
|
||||
const outletId = graphId("outlet", [item.id]);
|
||||
addNode({
|
||||
id: outletId,
|
||||
kind: "outlet",
|
||||
@@ -350,7 +352,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
evidence: evidenceFor(`outlet ${item.id}`),
|
||||
});
|
||||
connect(
|
||||
`${outletId}:source`,
|
||||
graphId("outlet-edge", [item.id, "source"]),
|
||||
sourceId,
|
||||
outletId,
|
||||
"contains",
|
||||
@@ -358,7 +360,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
);
|
||||
if (destinationId === null || !nodes.has(destinationId))
|
||||
connect(
|
||||
`${outletId}:destination`,
|
||||
graphId("outlet-edge", [item.id, "destination"]),
|
||||
outletId,
|
||||
null,
|
||||
"outlet_to",
|
||||
@@ -367,7 +369,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
);
|
||||
else
|
||||
connect(
|
||||
`${outletId}:destination`,
|
||||
graphId("outlet-edge", [item.id, "destination"]),
|
||||
outletId,
|
||||
destinationId,
|
||||
"outlet_to",
|
||||
@@ -376,7 +378,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
} else if (item.kind === "segue") {
|
||||
if (destinationId === null || !nodes.has(destinationId))
|
||||
connect(
|
||||
`${prefix}segue:${index}`,
|
||||
graphId("segue", [index]),
|
||||
sourceId,
|
||||
null,
|
||||
"segue_to",
|
||||
@@ -385,7 +387,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
);
|
||||
else
|
||||
connect(
|
||||
`${prefix}segue:${index}`,
|
||||
graphId("segue", [index]),
|
||||
sourceId,
|
||||
destinationId,
|
||||
"segue_to",
|
||||
@@ -393,7 +395,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
);
|
||||
} else {
|
||||
connect(
|
||||
`${prefix}unknown-connection:${index}`,
|
||||
graphId("unknown-connection", [index]),
|
||||
sourceId,
|
||||
null,
|
||||
"contains",
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "./javascriptStaticAnalysisHelpers.js";
|
||||
import {
|
||||
propertyName,
|
||||
semanticStaticPropertyName,
|
||||
semanticStaticPropertyKey,
|
||||
} from "./javascriptAstValues.js";
|
||||
import type { JavaScriptFindingContext } from "./javascriptStaticAnalysisState.js";
|
||||
|
||||
@@ -26,6 +26,8 @@ interface NativeBindingInput {
|
||||
readonly kind: ElectronNativeAddonBindingFinding["binding_kind"];
|
||||
readonly moduleKind: ElectronNativeAddonBindingFinding["module_kind"];
|
||||
readonly members: readonly string[];
|
||||
readonly namespaceAccess: boolean;
|
||||
readonly dynamicMemberAccess: boolean;
|
||||
}
|
||||
|
||||
/** Inspect JavaScript-side imports and re-exports of native .node addons. */
|
||||
@@ -45,10 +47,10 @@ const inspectImport = (
|
||||
context: JavaScriptFindingContext,
|
||||
): void => {
|
||||
if (!isNativeSpecifier(node.source.value, "import")) return;
|
||||
const members = node.specifiers.map((specifier) => {
|
||||
if (t.isImportDefaultSpecifier(specifier)) return "default";
|
||||
if (t.isImportNamespaceSpecifier(specifier)) return "*";
|
||||
return propertyName(specifier.imported) || "[dynamic-import]";
|
||||
const members = node.specifiers.flatMap((specifier) => {
|
||||
if (t.isImportDefaultSpecifier(specifier)) return ["default"];
|
||||
if (t.isImportNamespaceSpecifier(specifier)) return [];
|
||||
return [propertyName(specifier.imported)];
|
||||
});
|
||||
addBinding({
|
||||
context,
|
||||
@@ -57,6 +59,10 @@ const inspectImport = (
|
||||
kind: "import",
|
||||
moduleKind: "import",
|
||||
members,
|
||||
namespaceAccess: node.specifiers.some((value) =>
|
||||
t.isImportNamespaceSpecifier(value),
|
||||
),
|
||||
dynamicMemberAccess: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -70,9 +76,10 @@ const inspectNamedExport = (
|
||||
!isNativeSpecifier(node.source.value, "import")
|
||||
)
|
||||
return;
|
||||
const members = node.specifiers.map((specifier) => {
|
||||
if (t.isExportSpecifier(specifier)) return propertyName(specifier.local);
|
||||
return "*";
|
||||
const members = node.specifiers.flatMap((specifier) => {
|
||||
if (t.isExportSpecifier(specifier)) return [propertyName(specifier.local)];
|
||||
if (t.isExportDefaultSpecifier(specifier)) return ["default"];
|
||||
return [];
|
||||
});
|
||||
addBinding({
|
||||
context,
|
||||
@@ -81,6 +88,10 @@ const inspectNamedExport = (
|
||||
kind: "re-export",
|
||||
moduleKind: "import",
|
||||
members,
|
||||
namespaceAccess: node.specifiers.some((value) =>
|
||||
t.isExportNamespaceSpecifier(value),
|
||||
),
|
||||
dynamicMemberAccess: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -99,7 +110,9 @@ const inspectExportAll = (
|
||||
specifier: node.source.value,
|
||||
kind: "re-export",
|
||||
moduleKind: "import",
|
||||
members: ["*"],
|
||||
members: [],
|
||||
namespaceAccess: true,
|
||||
dynamicMemberAccess: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -115,8 +128,7 @@ const inspectRequireBinding = (
|
||||
specifier: required.specifier,
|
||||
kind: "require",
|
||||
moduleKind: "require",
|
||||
members:
|
||||
required.member === null ? bindingMembers(node.id) : [required.member],
|
||||
...(required.hasMemberAccess ? required.access : bindingMembers(node.id)),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -132,27 +144,33 @@ const inspectReExport = (
|
||||
specifier: required.specifier,
|
||||
kind: "re-export",
|
||||
moduleKind: "require",
|
||||
members: [required.member ?? exportedMember(node.left) ?? "*"],
|
||||
...required.access,
|
||||
});
|
||||
};
|
||||
|
||||
const addBinding = (input: NativeBindingInput): void => {
|
||||
const { context, node, specifier, kind } = input;
|
||||
const unique = [
|
||||
...new Set(input.members.filter((member) => member !== "")),
|
||||
].sort(compareUnicodeCodePoints);
|
||||
const members = unique.length === 0 ? ["*"] : unique;
|
||||
const members = [...new Set(input.members)].sort(compareUnicodeCodePoints);
|
||||
addLocatedFinding(context, {
|
||||
collection: context.accumulator.nativeAddonBindings,
|
||||
// Members are a variable-length source-derived list: compositeKey keeps
|
||||
// ["a\0b"] distinct from ["a", "b"] instead of collapsing one binding.
|
||||
key: compositeKey(["native-addon-binding", kind, specifier, members]),
|
||||
key: compositeKey([
|
||||
"native-addon-binding",
|
||||
kind,
|
||||
specifier,
|
||||
members,
|
||||
input.namespaceAccess,
|
||||
input.dynamicMemberAccess,
|
||||
]),
|
||||
node,
|
||||
value: {
|
||||
specifier,
|
||||
binding_kind: kind,
|
||||
module_kind: input.moduleKind,
|
||||
members,
|
||||
namespace_access: input.namespaceAccess,
|
||||
dynamic_member_access: input.dynamicMemberAccess,
|
||||
module_key: null,
|
||||
location: range(node),
|
||||
},
|
||||
@@ -162,13 +180,20 @@ const addBinding = (input: NativeBindingInput): void => {
|
||||
const nativeRequire = (
|
||||
node: t.Node | null | undefined,
|
||||
):
|
||||
| { readonly specifier: string; readonly member: string | null }
|
||||
| {
|
||||
readonly specifier: string;
|
||||
readonly hasMemberAccess: boolean;
|
||||
readonly access: NativeMemberAccess;
|
||||
}
|
||||
| undefined => {
|
||||
let member: string | null = null;
|
||||
let hasMemberAccess = false;
|
||||
while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
if (!t.isNode(node.object)) return undefined;
|
||||
const property = semanticStaticPropertyName(node.property, node.computed);
|
||||
if (member === null && property !== "") member = property;
|
||||
const property = semanticStaticPropertyKey(node.property, node.computed);
|
||||
// Walking inward leaves the first member consumed from the addon itself.
|
||||
member = property;
|
||||
hasMemberAccess = true;
|
||||
node = node.object;
|
||||
}
|
||||
if (!t.isCallExpression(node)) return undefined;
|
||||
@@ -181,34 +206,52 @@ const nativeRequire = (
|
||||
return undefined;
|
||||
const specifier = stringValue(argumentNode(node.arguments[0]));
|
||||
return specifier !== undefined && isNativeSpecifier(specifier, "require")
|
||||
? { specifier, member }
|
||||
? {
|
||||
specifier,
|
||||
hasMemberAccess,
|
||||
access: {
|
||||
members: member === null ? [] : [member],
|
||||
namespaceAccess: !hasMemberAccess,
|
||||
dynamicMemberAccess: hasMemberAccess && member === null,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const bindingMembers = (pattern: t.Node): string[] => {
|
||||
if (t.isIdentifier(pattern)) return ["*"];
|
||||
if (!t.isObjectPattern(pattern)) return ["*"];
|
||||
return pattern.properties.map((property) => {
|
||||
if (t.isRestElement(property)) return "*";
|
||||
return propertyName(property.key) || "[dynamic-import]";
|
||||
});
|
||||
interface NativeMemberAccess {
|
||||
readonly members: readonly string[];
|
||||
readonly namespaceAccess: boolean;
|
||||
readonly dynamicMemberAccess: boolean;
|
||||
}
|
||||
|
||||
const bindingMembers = (pattern: t.Node): NativeMemberAccess => {
|
||||
if (!t.isObjectPattern(pattern))
|
||||
return { members: [], namespaceAccess: true, dynamicMemberAccess: false };
|
||||
const members: string[] = [];
|
||||
let namespaceAccess = false;
|
||||
let dynamicMemberAccess = false;
|
||||
for (const property of pattern.properties) {
|
||||
if (t.isRestElement(property)) {
|
||||
namespaceAccess = true;
|
||||
continue;
|
||||
}
|
||||
const key = semanticStaticPropertyKey(property.key, property.computed);
|
||||
if (key === null) dynamicMemberAccess = true;
|
||||
else members.push(key);
|
||||
}
|
||||
return { members, namespaceAccess, dynamicMemberAccess };
|
||||
};
|
||||
|
||||
const isModuleExport = (node: t.Node): boolean => {
|
||||
const name = calleeName(node);
|
||||
return (
|
||||
name === "module.exports" ||
|
||||
name.startsWith("module.exports.") ||
|
||||
name.startsWith("exports.")
|
||||
);
|
||||
};
|
||||
|
||||
const exportedMember = (node: t.Node): string | undefined => {
|
||||
const name = calleeName(node);
|
||||
if (name.startsWith("module.exports."))
|
||||
return name.slice("module.exports.".length);
|
||||
if (name.startsWith("exports.")) return name.slice("exports.".length);
|
||||
return undefined;
|
||||
const keys: (string | null)[] = [];
|
||||
while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
keys.unshift(semanticStaticPropertyKey(node.property, node.computed));
|
||||
node = node.object;
|
||||
}
|
||||
if (!t.isIdentifier(node)) return false;
|
||||
return node.name === "exports"
|
||||
? keys.length > 0
|
||||
: node.name === "module" && keys[0] === "exports";
|
||||
};
|
||||
|
||||
const isNativeSpecifier = (
|
||||
|
||||
@@ -163,7 +163,11 @@ export interface ElectronNativeAddonBindingFinding {
|
||||
readonly specifier: string;
|
||||
readonly binding_kind: "import" | "require" | "re-export";
|
||||
readonly module_kind: "import" | "require";
|
||||
/** Exact requested addon member keys; namespace and dynamic access are separate. */
|
||||
readonly members: readonly string[];
|
||||
/** Omitted in legacy findings; omission means unknown, not false. */
|
||||
readonly namespace_access?: boolean;
|
||||
readonly dynamic_member_access?: boolean;
|
||||
readonly module_key: string | null;
|
||||
readonly location: JavaScriptSourceRange;
|
||||
}
|
||||
|
||||
@@ -257,7 +257,15 @@ const semanticValue = (
|
||||
return (
|
||||
joined(
|
||||
properties.specifier,
|
||||
stringList(properties.requested_members).join(","),
|
||||
compositeKey([
|
||||
stringList(properties.requested_members),
|
||||
typeof properties.namespace_access === "boolean"
|
||||
? properties.namespace_access
|
||||
: null,
|
||||
typeof properties.dynamic_member_access === "boolean"
|
||||
? properties.dynamic_member_access
|
||||
: null,
|
||||
]),
|
||||
) ?? firstLabel(node)
|
||||
);
|
||||
if (
|
||||
|
||||
@@ -103,7 +103,12 @@ const nativeExportNodes = (
|
||||
const requestedExports = (nodes: readonly ApplicationNode[]): string[] => {
|
||||
const values = nodes.flatMap((node) =>
|
||||
node.observations.flatMap((observation) => [
|
||||
...(observation.label === null ? [] : [observation.label]),
|
||||
// Display labels can summarize several names or namespace access.
|
||||
// Only legacy observations without structured member facts need them.
|
||||
...(observation.label === null ||
|
||||
Array.isArray(observation.properties.requested_members)
|
||||
? []
|
||||
: [observation.label]),
|
||||
...stringArray(observation.properties.requested_members),
|
||||
...stringArray(observation.properties.members),
|
||||
]),
|
||||
|
||||
Reference in New Issue
Block a user