UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

831 lines 41.9 kB
"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, '&quot;')}"]`; 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