@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
831 lines • 41.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.cranPageUrl = cranPageUrl;
exports.cranMirrorSourceUrl = cranMirrorSourceUrl;
exports.rSourceRef = rSourceRef;
exports.rSourceUrl = rSourceUrl;
exports.rdrrDocUrl = rdrrDocUrl;
exports.signatureFunctionInfo = signatureFunctionInfo;
exports.signaturePackageInfo = signaturePackageInfo;
exports.signatureQueryCompleter = signatureQueryCompleter;
exports.executeSignatureQuery = executeSignatureQuery;
const semver_1 = require("semver");
const reader_1 = require("../../../project/sigdb/reader");
const sigdb_version_1 = require("../../../project/sigdb/sigdb-version");
const schema_1 = require("../../../project/sigdb/schema");
const attached_packages_1 = require("../../../project/attached-packages");
const manifest_1 = require("../../../project/sigdb/manifest");
const query_fn_props_1 = require("../../../dataflow/environments/query-fn-props");
const built_in_props_1 = require("../../../dataflow/environments/built-in-props");
const identifier_1 = require("../../../dataflow/environments/identifier");
const r_version_1 = require("../../../util/r-version");
const r_base_packages_1 = require("../../../util/r-base-packages");
const mermaid_1 = require("../../../util/mermaid/mermaid");
const resolve_helper_1 = require("../../../dataflow/environments/resolve-helper");
/** the CRAN package landing page (only meaningful for CRAN packages, not base R) */
function cranPageUrl(pkg) {
return `https://cran.r-project.org/package=${encodeURIComponent(pkg)}`;
}
/** whether a pattern uses glob wildcards (`*`, `?`) */
function hasGlob(pattern) {
return pattern !== undefined && /[*?]/.test(pattern);
}
/** compile a glob (`*` matches any run, `?` matches one char) into an anchored, case-sensitive RegExp */
function globToRegExp(glob) {
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.');
return new RegExp(`^${escaped}$`);
}
/** a name matcher: exact equality, or a glob test when the pattern uses wildcards */
function nameMatcher(pattern) {
if (hasGlob(pattern)) {
const re = globToRegExp(pattern);
return name => re.test(name);
}
return name => name === pattern;
}
/** whether a version spec can match more than one release (a glob or a semver range, not a single exact version) */
function isMultiVersion(spec) {
return /[*?xX]/.test(spec) || /[<>~^=]/.test(spec) || spec.includes('||') || spec.includes(' - ');
}
/** a version matcher for a spec: glob (`3.*`), semver range (`>=3.0.0`, `3.x`), or an exact version */
function versionMatcher(spec) {
if (hasGlob(spec)) {
const re = globToRegExp(spec);
return v => re.test(v);
}
const range = (0, semver_1.validRange)(spec, { loose: true });
if (range !== null) {
return v => {
const parsed = r_version_1.RVersion.parse(v);
return parsed !== undefined ? (0, semver_1.satisfies)(parsed, range, { loose: true, includePrerelease: true }) : v === spec;
};
}
return v => v === spec;
}
/** whether the query narrows results by parameter shape (parameter names and/or a required-parameter count) */
function hasParameterFilter(q) {
return (q.parameters?.length ?? 0) > 0 || q.requiredParameters !== undefined;
}
/** a predicate over a decoded function for the query's parameter filters, or `undefined` when none are set */
function parameterFilter(q) {
if (!hasParameterFilter(q)) {
return undefined;
}
const nameMatchers = q.parameters?.map(nameMatcher);
const required = q.requiredParameters;
return fn => {
if (nameMatchers && !nameMatchers.every(m => fn.signature.some(p => m(p.name)))) {
return false;
}
// required = no default; `...` is never a required parameter to provide
return required === undefined || fn.signature.filter(p => p.name !== '...' && !p.optional).length === required;
};
}
/** the version strings of a package the loaded source can answer (dated releases, base-R core releases, and the latest) */
function availableVersions(src, pkg) {
return (0, reader_1.availableVersionEntries)(src, pkg).map(e => e.version);
}
/** a predicate selecting a package release by the `@version` spec: a date bound (`<=2026`, `>=2021.05`) or a version (exact/glob/range) */
function releaseMatcher(spec) {
const byDate = (0, sigdb_version_1.releaseDateBound)(spec);
if (byDate) {
return e => byDate(e.date);
}
const byVersion = versionMatcher(spec);
return e => byVersion(e.version);
}
/** read-only CRAN GitHub mirror base; `github.com/cran/<pkg>` mirrors every CRAN package and tags each release */
const CranGithubMirror = 'https://github.com/cran';
/** the mirror repository of a CRAN package */
function cranMirrorRepoUrl(pkg) {
return `${CranGithubMirror}/${encodeURIComponent(pkg)}`;
}
/** deep-link a definition into the CRAN mirror at the package's version tag (falling back to `HEAD`) */
function cranMirrorSourceUrl(pkg, version, file, line) {
const ref = version ? encodeURIComponent(version) : 'HEAD';
const anchor = line !== undefined && line >= 0 ? `#L${line}` : '';
return `${cranMirrorRepoUrl(pkg)}/blob/${ref}/${file}${anchor}`;
}
/** read-only GitHub mirror of R's own SVN; base packages live under `src/library/<pkg>` */
const RSourceMirror = 'https://github.com/wch/r-source';
/**
* The mirror ref holding an R version. The mirror carries no tags, only a `R-<major>-<minor>-branch` per release
* series, so a link is exact to the minor release and points at its latest patch; `trunk` stands in when the version
* is unknown.
*/
function rSourceRef(version) {
const series = /^(\d+)\.(\d+)/.exec(version ?? '');
return series ? `R-${series[1]}-${series[2]}-branch` : 'trunk';
}
/** deep-link a base-R definition into the R sources mirror at the release series of `version` */
function rSourceUrl(pkg, version, file, line) {
const anchor = line !== undefined && line >= 0 ? `#L${line}` : '';
return `${RSourceMirror}/blob/${rSourceRef(version)}/src/library/${encodeURIComponent(pkg)}/${file}${anchor}`;
}
/** function/topic names that map cleanly to a man page (skip operators like `+.gg`, `[.data.frame`; Rd topics allow hyphens, e.g. `dplyr-package`) */
const RdrrTopicName = /^[A-Za-z.][A-Za-z0-9._-]*$/;
/** best-effort rdrr.io documentation link: `/r/<pkg>/<fn>` for base R, `/cran/<pkg>/man/<fn>` for CRAN */
function rdrrDocUrl(pkg, fn, opts) {
if (!RdrrTopicName.test(fn)) {
return undefined;
}
if (opts.base) {
return `https://rdrr.io/r/${pkg}/${fn}.html`;
}
if (opts.cran) {
return `https://rdrr.io/cran/${pkg}/man/${fn}.html`;
}
return undefined;
}
/**
* The `.Rd` help source of a topic *at the queried version*, on the same mirrors the source links use. rdrr.io only
* serves a package's current release, so {@link rdrrDocUrl} silently answers for the wrong version whenever an older
* one was asked for; this link cannot drift.
*/
function manPageUrl(pkg, topic, version, opts) {
if (!RdrrTopicName.test(topic)) {
return undefined;
}
if (opts.base) {
return rSourceUrl(pkg, version, `man/${topic}.Rd`);
}
return opts.cran ? cranMirrorSourceUrl(pkg, version, `man/${topic}.Rd`) : undefined;
}
/** the doc links for a function: its help topic (else its name), or none when it is proven undocumented (`no-doc`) */
function docUrlsFor(pkg, fn, version, base, cran) {
if (fn.props.includes('no-doc')) {
return {};
}
const topic = fn.topic ?? fn.name;
const doc = rdrrDocUrl(pkg, topic, { base, cran });
const man = manPageUrl(pkg, topic, version, { base, cran });
return { ...(doc ? { docUrl: doc } : {}), ...(man ? { manUrl: man } : {}) };
}
/** the source link of a definition: the CRAN mirror at its version tag, or the R sources mirror for a base package */
function sourceUrlFor(pkg, fn, version, base, cran) {
if (!fn.file) {
return undefined;
}
if (base) {
return rSourceUrl(pkg, version, fn.file, fn.line);
}
return cran ? cranMirrorSourceUrl(pkg, version, fn.file, fn.line) : undefined;
}
/** the trailing fields shared by every function view: definition location, source link, and documentation links */
function locationFields(pkg, fn, version, base, cran) {
const source = sourceUrlFor(pkg, fn, version, base, cran);
return {
...(fn.file ? { file: fn.file } : {}),
...(fn.line >= 0 ? { line: fn.line } : {}),
...(source ? { sourceUrl: source } : {}),
...docUrlsFor(pkg, fn, version, base, cran)
};
}
/** the {@link CallProp}/{@link ArgProp} bits of `props`, lowercased, as the names to print */
function propNames(props, of) {
return Object.entries(of).filter(([, v]) => typeof v === 'number' && (props & v) !== 0).map(([k]) => k.toLowerCase());
}
/** the view of one {@link BuiltInFnInfo}: every declared parameter with what it is used for, and what comes back */
function flowrViewOf(info, sigParams) {
/* every declared parameter, even one flowR says nothing about, so the answer is the whole signature */
const args = (info.sig ?? []).map(([n, p]) => ({ name: n, roles: propNames(p, built_in_props_1.ArgProp) }));
const params = args.map(a => a.name);
const returns = info.sig?.find(([, p]) => (p & built_in_props_1.ArgProp.Alias) !== 0)?.[0];
/* flowR usually declares only the parameters it models, which is no disagreement as long as they line up */
const same = params.every((n, i) => n === sigParams[i]);
return {
props: propNames(info.props ?? 0, built_in_props_1.CallProp),
...(args.length > 0 ? { args } : {}),
...(returns !== undefined ? { returns } : {}),
...(params.length > 0 && !same ? { parameters: params } : {})
};
}
/**
* What flowR states about `pkg::name` itself, resolved in `env` so that a configured built-in wins over the
* default one. The parameter names are only reported when they differ from the ones `sigParams` records.
*/
function flowrView(env, pkg, name, sigParams) {
const info = env === undefined ? undefined : (0, query_fn_props_1.queryFnProps)(identifier_1.Identifier.make(name, pkg), { environment: env });
if (info === undefined || (info.props === undefined && info.sig === undefined)) {
return undefined;
}
return flowrViewOf(info, sigParams);
}
/**
* The view of a call flowR models itself but the signature database has no entry for: the primitives and
* operators (`+`, `[`, `if`) that never appear in a package's sources, and anything a flowR configuration
* adds. `pkg` narrows the lookup when the query named one, otherwise the built-in's own namespace is reported.
*/
function flowrOnlyFunctionInfo(env, pkg, name) {
if (env === undefined) {
return undefined;
}
const resolved = resolve_helper_1.Resolve.byNameAndType(pkg === undefined ? name : identifier_1.Identifier.make(name, pkg), env, identifier_1.ReferenceType.Function);
const definition = resolved?.find(d => d.type === identifier_1.ReferenceType.BuiltInFunction);
if (definition === undefined) {
return undefined;
}
const info = (0, query_fn_props_1.queryFnProps)(definition.name ?? name, { environment: env });
if (info === undefined || (info.props === undefined && info.sig === undefined)) {
return undefined;
}
const namespace = definition.name === undefined ? undefined : identifier_1.Identifier.getNamespace(definition.name);
if (pkg !== undefined && namespace !== undefined && namespace !== pkg) {
return undefined;
}
return {
name,
package: namespace ?? pkg ?? 'base',
flowrOnly: true,
exported: true,
properties: [],
/* flowR states no defaults, so `required` stays `false` throughout; whether R forces a parameter it does know */
parameters: (info.sig ?? []).map(([n, p]) => ({ name: n, required: false, forced: (p & built_in_props_1.ArgProp.Forced) !== 0 })),
callees: [],
flowr: flowrViewOf(info, [])
};
}
/** the decoded view of one function, adding the CRAN-mirror source link */
function decodedToView(pkg, fn, version, opts) {
return {
name: fn.name,
package: pkg,
...(version !== undefined ? { version } : {}),
exported: fn.exported,
properties: fn.props,
parameters: fn.signature.map(p => ({
name: p.name,
required: !p.optional,
forced: p.forced,
...(p.default !== undefined ? { default: p.default } : {})
})),
callees: fn.callees,
...locationFields(pkg, fn, version, opts.base, opts.cran)
};
}
/**
* The detailed view of a single function within a package: its signature (parameters, forced/optional,
* defaults), properties, definition location, call graph, and for a CRAN package a deep link into the
* read-only CRAN GitHub mirror. `version` defaults to the source's latest; `undefined` when the source does
* not carry that function.
*/
function signatureFunctionInfo(src, pkg, fnName, version, env) {
const fns = src.functions(pkg, version) ?? src.functions(pkg);
const fn = fns?.find(f => f.name === fnName);
if (fn === undefined) {
return undefined;
}
const exports = src.lookup(pkg, version) ?? src.lookup(pkg);
const view = decodedToView(pkg, fn, exports?.version, { cran: exports?.cran ?? false, base: src.isBaseR(pkg) });
// an S3 generic's methods are the `<generic>.<class>` functions that resolve back to it (see s3MethodParts);
// the reverse links a method to the generic it dispatches for
const methods = (fns ?? [])
.filter(f => f.name !== fn.name && f.name.startsWith(fn.name + '.') && s3MethodParts(src, pkg, fns, f)?.generic === fn.name)
.map(f => f.name)
.sort();
const s3method = s3MethodParts(src, pkg, fns, fn);
const flowr = flowrView(env, pkg, fnName, view.parameters.map(p => p.name));
return {
...view,
...(flowr ? { flowr } : {}),
...(methods.length > 0 ? { s3generic: true, s3methods: methods } : {}),
...(s3method ? { s3method } : {})
};
}
/** whether `fn` is an S3 generic: its body dispatches via `UseMethod`, so `<name>.<class>` functions can be its methods */
function dispatchesGeneric(fn) {
return fn.callees.includes('UseMethod');
}
/** the function named `generic`, resolved in `pkg` first, then base R, with the package it was found in */
function resolveGeneric(src, pkg, fns, generic) {
const local = fns?.find(f => f.name === generic);
if (local) {
return { fn: local, package: pkg };
}
for (const base of (0, r_base_packages_1.baseRPackages)()) {
if (base !== pkg) {
const fn = src.functionByName(base, generic);
if (fn) {
return { fn, package: base };
}
}
}
return undefined;
}
/** whether `cls` is a class a loaded package registers S3 methods for (the method's own package, or base R) */
function isKnownS3Class(src, pkg, cls) {
if (src.lookup(pkg)?.s3Classes.includes(cls)) {
return true;
}
for (const base of (0, r_base_packages_1.baseRPackages)()) {
if (base !== pkg && src.lookup(base)?.s3Classes.includes(cls)) {
return true;
}
}
return false;
}
/** the `generic.class` readings of a dotted name, longest generic first (`as.data.frame.matrix` before `as.data`) */
function* dottedSplits(name) {
for (let dot = name.lastIndexOf('.'); dot > 0; dot = name.lastIndexOf('.', dot - 1)) {
yield [name.slice(0, dot), name.slice(dot + 1)];
}
}
/**
* The generic and dispatch class of an S3 method named `generic.class`, or `undefined` when `fn` is not one.
*
* The crawled `s3-method` property settles it when present. It is missing for a base method whose generic lives in
* another package (`stats` never flags `print.acf`, since `print` is in `base`), so then the prefix must name a
* dispatching generic and the suffix a registered class. Dropping either check would split `data.frame` or `t.test`.
*/
function s3MethodParts(src, pkg, fns, fn) {
const flagged = fn.props.includes('s3-method');
for (const [generic, cls] of dottedSplits(fn.name)) {
const g = resolveGeneric(src, pkg, fns, generic);
if (g && (flagged || (dispatchesGeneric(g.fn) && isKnownS3Class(src, pkg, cls)))) {
return { generic, class: cls, package: g.package };
}
}
return undefined;
}
/** how many nodes the call-graph render is capped at (cross-package expansion could otherwise explode into base R) */
const CallGraphMaxNodes = 300;
/**
* A mermaid.live link for the transitive call graph reachable from `pkg::root`, resolved across package borders:
* a bare callee is attributed to the first namespace that exports it (the calling package, then its attached
* `Depends`/`Imports`, then base R), qualified as `owner::fn`, and expanded there. Base R calls are qualified leaves
* (their internals are noise); explicit `pkg::fn` calls resolve directly. Bounded by {@link CallGraphMaxNodes}.
*/
function signatureCallGraphUrl(src, pkg, version, root) {
const bases = new Set((0, r_base_packages_1.baseRPackages)());
const exportsCache = new Map();
const exportsOf = (p, v) => {
const key = `${p}\0${v ?? ''}`;
let s = exportsCache.get(key);
if (s === undefined) {
exportsCache.set(key, s = new Set(src.functions(p, v)?.map(f => f.name) ?? src.lookup(p, v)?.exported ?? []));
}
return s;
};
const attachedDeps = (p, v) => (src.dependencies(p, v) ?? []).filter(d => d.type === 0 /* DepType.Depends */ || d.type === 1 /* DepType.Imports */).map(d => d.name);
/** resolve a callee named in `p@v` to its owning `{ owner, name }`, or undefined when no loaded namespace exports it */
const resolve = (p, v, callee) => {
const q = /^([A-Za-z][\w.]*):::?(.+)$/.exec(callee);
if (q) {
return { owner: q[1], name: q[2] };
}
if (exportsOf(p, v).has(callee)) {
return { owner: p, name: callee };
}
for (const dep of attachedDeps(p, v)) {
if (exportsOf(dep, src.latestVersion(dep)?.str).has(callee)) {
return { owner: dep, name: callee };
}
}
// base R (incl. C-level primitives absent from the sigdb: `c`, `is.na`, ...) via flowR's base export/primitive store
const base = (0, r_base_packages_1.baseRExportOwner)(callee);
return base !== undefined ? { owner: base, name: callee } : undefined;
};
// each distinct node is keyed by `owner::name`, remembering its owning package (for the subgraph) and short label
const nodes = new Map();
const node = (owner, short) => {
const key = owner ? `${owner}::${short}` : short;
let n = nodes.get(key);
if (n === undefined) {
nodes.set(key, n = { id: `n${nodes.size}`, owner, short });
}
return n.id;
};
const rootId = node(pkg, root);
const edges = [];
const seen = new Set();
const queue = [{ owner: pkg, ver: version, name: root, id: rootId }];
while (queue.length > 0 && nodes.size < CallGraphMaxNodes) {
const cur = queue.pop();
const key = `${cur.owner}::${cur.name}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
for (const callee of src.functionByName(cur.owner, cur.name, cur.ver)?.callees ?? []) {
const r = resolve(cur.owner, cur.ver, callee);
edges.push(` ${cur.id} --> ${node(r?.owner, r ? r.name : callee)}`);
// expand into a resolved same/other CRAN package (not base R, whose internals explode the graph), avoiding cycles
if (r && !bases.has(r.owner) && !seen.has(`${r.owner}::${r.name}`) && nodes.size < CallGraphMaxNodes) {
queue.push({ owner: r.owner, ver: r.owner === cur.owner ? cur.ver : src.latestVersion(r.owner)?.str, name: r.name, id: node(r.owner, r.name) });
}
}
}
// group nodes into a `subgraph` per owning package; unqualified/unresolved calls stay ungrouped
const byOwner = new Map();
const ungrouped = [];
for (const { id, owner, short } of nodes.values()) {
const decl = `${id}["${short.replace(/"/g, '"')}"]`;
if (owner === undefined) {
ungrouped.push(` ${decl}`);
}
else {
let decls = byOwner.get(owner);
if (decls === undefined) {
byOwner.set(owner, decls = []);
}
decls.push(` ${decl}`);
}
}
const subgraphs = [...byOwner].flatMap(([owner, decls]) => [` subgraph ${owner}`, ...decls, ' end']);
return mermaid_1.Mermaid.codeToUrl(['flowchart LR', ...subgraphs, ...ungrouped, ...edges].join('\n'));
}
/** the full view of a package: version, kind, export breakdown, dependencies, and every function's view */
function signaturePackageInfo(src, pkg, resolved) {
const exports = src.lookup(pkg, resolved) ?? src.lookup(pkg);
if (exports === undefined) {
return undefined;
}
const base = src.isBaseR(pkg);
const fns = src.functions(pkg, resolved) ?? src.functions(pkg) ?? [];
const fnNames = new Set(fns.map(f => f.name));
const constants = exports.exported.filter(n => !fnNames.has(n));
const deps = (src.dependencies(pkg, resolved) ?? src.dependencies(pkg) ?? [])
.map(d => ({ type: schema_1.DepTypeNames[d.type], name: d.name, ...(d.constraint ? { constraint: d.constraint } : {}) }));
const release = src.releaseDate(pkg, resolved);
const attaches = (0, attached_packages_1.attachedAlongside)(pkg, [src], resolved);
return {
name: pkg,
version: exports.version,
...(resolved && resolved !== exports.version ? { resolved } : {}),
base,
cran: exports.cran,
...(exports.cranUrl ? { cranUrl: exports.cranUrl } : {}),
...(exports.cran && !base ? { cranPage: cranPageUrl(pkg), repoUrl: cranMirrorRepoUrl(pkg) } : {}),
...(release && !Number.isNaN(release.getTime()) ? { releaseDate: release.toISOString().slice(0, 10) } : {}),
exportsTotal: exports.exported.length,
functionCount: exports.exported.length - constants.length,
constants,
internalCount: exports.internal.length,
deprecated: exports.deprecated,
...(base && src.coreVersions(pkg) ? { coreVersions: src.coreVersions(pkg)?.map(v => v.str) } : {}),
dependencies: deps,
...(attaches.length > 0 ? { attaches } : {}),
functions: fns.map(f => decodedToView(pkg, f, exports.version, { cran: exports.cran, base }))
};
}
/** a few near matches for a mistyped package/symbol (case-insensitive substring), for a friendly hint */
function suggest(candidates, query, limit = 6) {
const needle = query.toLowerCase();
const out = [];
for (const c of candidates) {
if (c.toLowerCase().includes(needle)) {
out.push(c);
if (out.length >= limit) {
break;
}
}
}
return out;
}
/** cap on wildcard-search hits, so a `* *` search cannot exhaust memory */
const MaxMatches = 500;
/** how many parameter names a match preview shows before eliding the rest with `…` */
const ParamPreviewCap = 4;
/**
* A short preview of a function's parameters for a match, when the query filters by parameter, kept in signature
* order (highlighting never reorders it): the matched parameters are always shown, padded with leading parameters
* for context up to {@link ParamPreviewCap} and elided with `…` when any are dropped. Also returns the matched names
* so the renderer can highlight them. `undefined` when no parameter filter is set.
*/
function matchedParamPreview(fn, q) {
if (!hasParameterFilter(q)) {
return undefined;
}
const names = fn.signature.map(p => p.name);
const matched = q.parameters?.length
? names.filter(n => q.parameters?.some(pat => nameMatcher(pat)(n)))
: [];
const show = new Set(matched.length > 0 ? matched : fn.signature.filter(p => p.name !== '...' && !p.optional).map(p => p.name));
for (const n of names) {
if (show.size >= ParamPreviewCap) {
break;
}
show.add(n);
}
const preview = names.filter(n => show.has(n));
if (preview.length < names.length) {
preview.push('…');
}
return { preview, matched };
}
/** a compact view for a wildcard search hit (signature/call-graph omitted; the JSON dump carries those per name) */
function compactMatch(pkg, fn, version, base, cran, params) {
return {
package: pkg,
name: fn.name,
exported: fn.exported,
...(version !== undefined ? { version } : {}),
...locationFields(pkg, fn, version, base, cran),
...(params && params.preview.length > 0 ? { parameters: params.preview } : {}),
...(params && params.matched.length > 0 ? { matchedParameters: params.matched } : {})
};
}
/** the versions of a package across every loaded source that holds it (current + history + any mounted extra) */
function allAvailableVersions(sources, pkg) {
const set = new Set();
for (const s of sources) {
if (s.has(pkg)) {
for (const v of availableVersions(s, pkg)) {
set.add(v);
}
}
}
return [...set].sort((a, b) => r_version_1.RVersion.compare(a, b));
}
/**
* The owning source that actually carries `version`: `current` is checked before `history`, so a
* latest-version query never decompresses the (large) history shard. Returns the first owner when no version
* was asked, or `undefined` when an explicit version is carried by none of them.
*/
function sourceForVersion(owning, pkg, version) {
if (version === undefined) {
return owning[0];
}
return owning.find(s => availableVersions(s, pkg).includes(version));
}
/** the message shown when a known package has no release matching the requested version, listing what is available */
function versionNotFoundMessage(pkg, lead, avail, base) {
// only nudge towards a full-history bundle when one is *not* already mounted (a single known version)
const hint = !base && avail.length <= 1
? ' Only the latest CRAN version is loaded; download the full history with `:signature download` (or mount one with `:signature add <path>`).'
: '';
return `${lead}${avail.length ? ` Available: ${avail.join(', ')}.` : ''}${hint}`;
}
/** run a wildcard search across the loaded sources: matching packages (no function), or matching functions */
function searchSources(sources, allNames, q) {
const cap = MaxMatches;
const pkgMatch = nameMatcher(q.package);
const matchedPkgs = [...allNames].filter(pkgMatch).sort();
const relMatch = q.version ? releaseMatcher(q.version) : undefined;
// every source holding a package (its latest lives in `current`, its older releases in `history`)
const owningOf = (pkg) => sources.filter(s => s.has(pkg));
// the versions of `pkg` matching the spec, unioned across all owning sources (so a `3.*`/date filter reaches history)
const matchingVersions = (owners, pkg, m) => [...new Set(owners.flatMap(s => (0, reader_1.availableVersionEntries)(s, pkg).filter(m).map(e => e.version)))];
const paramPred = parameterFilter(q);
// a parameter filter (with no function name) still means "search functions", not "list packages"
if (!q.function && paramPred === undefined) {
const packages = [];
let truncated = false;
for (const pkg of matchedPkgs) {
const owners = owningOf(pkg);
if (owners.length === 0) {
continue;
}
const base = owners[0].isBaseR(pkg);
// with a version filter, union the matching versions across all sources (so a `3.*`/date reaches history)
const versions = relMatch
? matchingVersions(owners, pkg, relMatch).sort((a, b) => r_version_1.RVersion.compare(a, b))
: undefined;
if (versions !== undefined && versions.length === 0) {
continue;
}
if (packages.length >= cap) {
truncated = true;
break;
}
const cran = (owners[0].lookup(pkg)?.cran ?? false) && !base;
const latest = owners[0].latestVersion(pkg)?.str;
packages.push({ name: pkg, base, cran, ...(latest ? { latest } : {}), ...(versions ? { versions } : {}), ...(cran ? { cranPage: cranPageUrl(pkg) } : {}) });
}
return { packages, truncated };
}
const fnMatch = q.function ? nameMatcher(q.function) : () => true;
// an exact function name lets us seek that one record (decoding only it) instead of decoding every function of
// every package, the difference between a fast `* ggplot` and one that decodes the whole database
const exactName = q.function !== undefined && !hasGlob(q.function);
const matches = [];
let searched = 0;
let truncated = false;
for (const pkg of matchedPkgs) {
const owners = owningOf(pkg);
if (owners.length === 0) {
continue;
}
const base = owners[0].isBaseR(pkg);
// map each version to the single source that owns it (current before history), computed once per package so a
// version-filtered function search does not rebuild the release list per candidate version
const ownerOf = new Map();
if (relMatch) {
for (const o of owners) {
for (const e of (0, reader_1.availableVersionEntries)(o, pkg)) {
if (!ownerOf.has(e.version) && relMatch(e)) {
ownerOf.set(e.version, o);
}
}
}
}
// versions to scan: with a filter, each matching release newest-first (scanned in its single owning source, so a
// release in more than one source is not double-counted, and the truncation cap keeps the newest); else just the latest
const versionsToScan = relMatch ? [...ownerOf.keys()].sort((a, b) => r_version_1.RVersion.compare(b, a)) : [undefined];
for (const v of versionsToScan) {
const s = v === undefined ? owners[0] : ownerOf.get(v) ?? owners[0];
const candidates = exactName
? (fn => fn ? [fn] : [])(s.functionByName(pkg, q.function, v))
: (s.functions(pkg, v) ?? s.functions(pkg) ?? []);
// resolve the (heavier) package export view only once a function actually matches, so non-matching
// packages in a wildcard search cost just the name lookup, not a full export derivation
let exports;
let looked = false;
for (const fn of candidates) {
if (!fnMatch(fn.name)) {
continue;
}
searched++;
if (paramPred && !paramPred(fn)) {
continue;
}
if (matches.length >= cap) {
truncated = true;
break;
}
if (!looked) {
exports = s.lookup(pkg, v) ?? s.lookup(pkg);
looked = true;
}
matches.push(compactMatch(pkg, fn, exports?.version, base, (exports?.cran ?? false) && !base, matchedParamPreview(fn, q)));
}
if (truncated) {
break;
}
}
if (truncated) {
break;
}
}
return { matches, matchCount: matches.length, searched, truncated, latestOnly: relMatch === undefined };
}
/** the discoverable bundle sources for Tab completion (process-wide cached; opening the manifest reads no shard) */
function completionSources() {
if (typeof process !== 'undefined' && process.env?.FLOWR_DISABLE_DEFAULT_SIGDB) {
return [];
}
const out = [];
for (const p of (0, manifest_1.defaultSigDbPaths)()) {
const src = (0, reader_1.getSharedSigSourceSync)(p);
if (src) {
out.push(src);
}
}
return out;
}
/** the package part of a spec token, dropping any `@version` and `::function` suffix */
function packageOf(spec) {
return spec.split('::')[0].split('@')[0];
}
/** cap on offered names, so an empty fragment does not dump the whole 24k-package set at the terminal */
const MaxCompletions = 200;
/**
* Tab-completer for `:query \@signature` / `:signature query`: package names in the first position, a package's
* function names in the second (and after `pkg::`). Version specs and flags are left alone. Enumerating functions
* decompresses the package's shard once (then cached); enumerating packages reads only the manifest.
*/
function signatureQueryCompleter(line, startingNewArg) {
const sources = completionSources();
if (sources.length === 0) {
return { completions: [] };
}
const capped = (names, prefix, decorate) => {
const all = [];
for (const n of names) {
const d = decorate(n); // filter on the offered text (e.g. `pkg::fn`), not the bare name
if (d.startsWith(prefix)) {
all.push(d);
}
}
if (all.length <= MaxCompletions) {
return all;
}
// too many to show: sample evenly across the sorted set so the offered names span the alphabet, not just
// its head; index 0 stays first so the ghost hint still previews the true best (alphabetically first) match
const stride = all.length / MaxCompletions;
return Array.from({ length: MaxCompletions }, (_, i) => all[Math.floor(i * stride)]);
};
const packageNames = () => [...new Set(sources.flatMap(s => s.packageNames()))].sort();
const functionsOf = (pkg) => {
const src = sources.find(s => s.has(pkg));
return src ? [...new Set((src.functions(pkg) ?? []).map(f => f.name))].sort() : [];
};
// first token: a package spec (`pkg`, `pkg::fn`, `pkg@ver`)
if (line.length === 0 || (line.length === 1 && !startingNewArg)) {
const token = line[0] ?? '';
const dbl = token.indexOf('::');
if (dbl >= 0) {
const pkg = packageOf(token), frag = token.slice(0, dbl + 2);
return { completions: capped(functionsOf(pkg), token, fn => `${frag}${fn} `), argumentPart: token };
}
if (token.includes('@')) {
return { completions: [] }; // typing a version, nothing to offer
}
return { completions: capped(packageNames(), token, p => `${p} `), argumentPart: token };
}
// second token: the function within the first token's package
if ((line.length === 1 && startingNewArg) || (line.length === 2 && !startingNewArg)) {
const frag = line.length === 2 ? line[1] : '';
return { completions: capped(functionsOf(packageOf(line[0])), frag, fn => `${fn} `), argumentPart: frag };
}
return { completions: [] };
}
/** the deduped shard load-state across all sharded sources (a shard id mounted by several sources folds into one, accessed/unpacked if any source is) */
function collectShardStatus(sources) {
const byId = new Map();
for (const s of sources.flatMap(src => src instanceof reader_1.SigDatabaseSet ? src.shardStatus() : [])) {
const prev = byId.get(s.id);
byId.set(s.id, prev ? { ...prev, accessed: prev.accessed || s.accessed, unpacked: prev.unpacked || s.unpacked } : s);
}
return [...byId.values()];
}
/**
* Executes the signature query. With no `package` it summarizes the loaded databases. A glob in `package`/`function`
* or a multi-version `version` triggers a wildcard search (matching packages or functions). Otherwise a single
* exact package (optionally at an exact `version`) yields its full view, or the detailed function view.
*/
// eslint-disable-next-line @typescript-eslint/require-await -- executor contract returns a Promise; the work is synchronous
async function executeSignatureQuery({ analyzer }, queries) {
const start = Date.now();
const q = queries[queries.length - 1] ?? { type: 'signature' };
const deps = analyzer.inspectContext().deps;
const databases = deps.loadedSignatureDatabases()
.map(d => ({ scope: d.scope, version: d.version, date: d.date }));
// the plugin's loaded sources (bundled default + $FLOWR_SIGDB + anything added at runtime), so the query
// reflects dynamically-mounted sources
const sources = deps.signatureSources();
const packages = new Set();
for (const s of sources) {
for (const n of s.packageNames()) {
packages.add(n);
}
}
const meta = () => ({ '.meta': { timing: Date.now() - start }, databases, packageCount: packages.size, sourceCount: sources.length });
/* the built-in environment answers for the primitives no package's sources contain, see flowrOnlyFunctionInfo */
const builtIn = (pkg, name) => flowrOnlyFunctionInfo(analyzer.inspectContext().env.makeCleanEnv(), pkg, name);
if (!q.package) {
// shard load-state is only shown in the summary, so it is only worth its filesystem probes here
return { ...meta(), shards: collectShardStatus(sources) };
}
// wildcard search: a glob in the package/function name, a version spec matching more than one release (a range or
// a date bound), or a parameter filter (which narrows a set of functions and so always goes through the search path)
if (hasGlob(q.package) || (q.function !== undefined && hasGlob(q.function)) || (q.version !== undefined && (isMultiVersion(q.version) || (0, sigdb_version_1.isDateBound)(q.version))) || hasParameterFilter(q)) {
const found = searchSources(sources, packages, q);
// a version glob against a single concrete, known package that matched no release: point at the available versions
// (the same guidance the exact-version path gives) instead of a bare "0 matched"
if ((found.matchCount === 0 || found.packages?.length === 0) && !hasGlob(q.package) && q.version !== undefined) {
const owning = sources.filter(s => s.has(q.package));
if (owning.length > 0) {
const avail = allAvailableVersions(owning, q.package);
return { ...meta(), message: versionNotFoundMessage(q.package, `no release of '${q.package}' matches '${q.version}'.`, avail, owning[0].isBaseR(q.package)) };
}
}
if (found.matchCount === 0 && q.function !== undefined && !hasGlob(q.function)) {
return { ...meta(), ...found, message: `No function named exactly '${q.function}'. Try a wildcard like '*${q.function}*'.` };
}
return { ...meta(), ...found };
}
// every source holding the package: `current` keeps the latest, `history` the older releases, so the resolved
// version must be looked up in whichever source actually has it (not just the first one found)
const owning = sources.filter(s => s.has(q.package));
if (owning.length === 0) {
// `@signature +` names no package but a call flowR models itself, so answer for that instead of not-found
const own = q.function === undefined ? builtIn(undefined, q.package) : undefined;
return own ? { ...meta(), function: own }
: { ...meta(), message: `The signature database does not know the package '${q.package}'.`, suggestions: suggest(packages, q.package) };
}
// the version to resolve against: an explicit `@version`, else the version flowR inferred for the script's
// dependency (which may be an older release that only `history` carries)
const version = q.version ?? deps.getDependency(q.package)?.resolvedVersion;
const src = sourceForVersion(owning, q.package, version);
if (q.version !== undefined && src === undefined) {
const avail = allAvailableVersions(owning, q.package);
return { ...meta(), message: versionNotFoundMessage(q.package, `'${q.package}@${q.version}' is not in the loaded database.`, avail, owning[0].isBaseR(q.package)) };
}
const resolvedSrc = src ?? owning[0];
if (q.function) {
const fn = signatureFunctionInfo(resolvedSrc, q.package, q.function, version, analyzer.inspectContext().env.makeCleanEnv());
if (fn) {
const cg = q.callGraph ? signatureCallGraphUrl(resolvedSrc, q.package, version, fn.name) : undefined;
return { ...meta(), function: cg ? { ...fn, callGraph: cg } : fn };
}
// a primitive like `base::+` has no entry in the package's sources, but flowR models it itself
const own = builtIn(q.package, q.function);
if (own) {
return { ...meta(), function: own };
}
const exports = resolvedSrc.lookup(q.package, version) ?? resolvedSrc.lookup(q.package);
const universe = new Set([
...(exports?.exported ?? []),
...(resolvedSrc.functions(q.package, version) ?? resolvedSrc.functions(q.package) ?? []).map(f => f.name)
]);
return {
...meta(),
package: signaturePackageInfo(resolvedSrc, q.package, version),
message: `'${q.package}' does not define '${q.function}'.`,
suggestions: suggest(universe, q.function)
};
}
return { ...meta(), package: signaturePackageInfo(resolvedSrc, q.package, version) };
}
//# sourceMappingURL=signature-query-executor.js.map