realapi-check 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/analyzer.js CHANGED
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import { createRequire } from "node:module";
3
3
  import ts from "typescript";
4
4
  import { collectProjectDeclarations, collectSourceFiles, isDeclarationFile, normalize, } from "./files.js";
5
- import { declaredDependencies, installedTypesPackages, isBuiltin, isInstalled, installedVersion, missingFromRegistry, packageNameOf, packageOfFile, usesPnp, } from "./packages.js";
5
+ import { declaredDependencies, installedTypesPackages, isBuiltin, isInstalled, installedVersion, missingFromRegistry, packageNameOf, packageOfFile, runtimeSources, usesPnp, } from "./packages.js";
6
6
  // TypeScript diagnostics that mean "the code uses an API the types don't have".
7
7
  const MISSING_EXPORT_CODES = new Set([2305, 2724, 2614, 2459, 2460]);
8
8
  const MISSING_MEMBER_CODES = new Set([2339, 2551]);
@@ -220,7 +220,26 @@ export async function check(opts = {}) {
220
220
  return undefined;
221
221
  if (kind === "missing-member" && isModuleType(ownerType))
222
222
  kind = "missing-export";
223
- return { kind, owner, ownerLabel: checker.typeToString(ownerType) };
223
+ const name = ts.isIdentifier(node) || ts.isStringLiteralLike(node) ? node.text : undefined;
224
+ // On a union, TypeScript errors unless *every* member has the property.
225
+ // If any member has it, the API is real and the code just needs narrowing.
226
+ if (name && ownerType.isUnion() && ownerType.types.some((t) => checker.getPropertyOfType(t, name))) {
227
+ return undefined;
228
+ }
229
+ // Long anonymous types read badly; name the expression instead.
230
+ let ownerLabel = checker.typeToString(ownerType);
231
+ const access = ts.isPropertyAccessExpression(parent) ? parent : undefined;
232
+ if (access && ownerLabel.length > 60)
233
+ ownerLabel = access.expression.getText();
234
+ // `doc.internal.getNumberOfPages()` when the real API is `doc.getNumberOfPages()`.
235
+ let suggestion;
236
+ if (name && access && ts.isPropertyAccessExpression(access.expression)) {
237
+ const grandparent = access.expression.expression;
238
+ if (checker.getPropertyOfType(checker.getTypeAtLocation(grandparent), name)) {
239
+ suggestion = `${grandparent.getText()}.${name}`;
240
+ }
241
+ }
242
+ return { kind, owner, ownerLabel, suggestion };
224
243
  };
225
244
  const issues = [];
226
245
  const checkedPackages = new Map();
@@ -308,6 +327,24 @@ export async function check(opts = {}) {
308
327
  const d = display(p);
309
328
  return d.version ? `${d.name}@${d.version}` : d.name;
310
329
  };
330
+ /**
331
+ * Plugins such as jspdf-autotable add members to their host's objects at
332
+ * runtime (`doc.lastAutoTable = table`) without typing them. If an
333
+ * imported package that depends on the owner assigns this member in its
334
+ * runtime code, the member is real.
335
+ */
336
+ const addedByPlugin = (owner, name) => {
337
+ const host = display(owner).name;
338
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
339
+ const assignment = new RegExp(`\\.${escaped}\\s*=(?!=)`);
340
+ for (const pkg of imported.values()) {
341
+ if (pkg.rawName === owner.rawName || !pkg.dependencies.includes(host))
342
+ continue;
343
+ if (runtimeSources(pkg.root).some((code) => assignment.test(code)))
344
+ return true;
345
+ }
346
+ return false;
347
+ };
311
348
  const displayFields = (p) => {
312
349
  const d = display(p);
313
350
  return { package: d.name, version: d.version };
@@ -343,7 +380,9 @@ export async function check(opts = {}) {
343
380
  for (const { sf, start, message: tsMessage, located, name } of found) {
344
381
  if (located.kind === "missing-member" && assigned.has(memberKey(located, name)))
345
382
  continue;
346
- const suggestion = suggestionFrom(tsMessage);
383
+ if (located.kind === "missing-member" && addedByPlugin(located.owner, name))
384
+ continue;
385
+ const suggestion = suggestionFrom(tsMessage) ?? located.suggestion;
347
386
  const where = versioned(located.owner);
348
387
  let message;
349
388
  if (located.kind === "missing-export") {
@@ -388,6 +427,8 @@ export async function check(opts = {}) {
388
427
  .find((t) => t.name === "deprecated")
389
428
  ?.text?.map((p) => p.text)
390
429
  .join("")
430
+ // `{@link register}` / `{@link Foo | text}` → `register` / `text`.
431
+ .replace(/\{@link(?:code|plain)?\s+([^}|]+?)(?:\s*\|\s*([^}]+))?\s*\}/g, (_, target, text) => text ?? target)
391
432
  .replace(/\s+/g, " ")
392
433
  .trim();
393
434
  const name = symbol.getName();
@@ -35,3 +35,5 @@ export declare function declaredDependencies(cwd: string): Set<string>;
35
35
  export declare function installedTypesPackages(cwd: string): string[];
36
36
  /** Returns true when the npm registry has no package with this name. */
37
37
  export declare function missingFromRegistry(name: string): Promise<boolean | undefined>;
38
+ /** The runtime entry files of a package (main, module, exports), read as text. */
39
+ export declare function runtimeSources(root: string): string[];
package/dist/packages.js CHANGED
@@ -173,3 +173,42 @@ export async function missingFromRegistry(name) {
173
173
  return undefined;
174
174
  }
175
175
  }
176
+ const sourceCache = new Map();
177
+ /** The runtime entry files of a package (main, module, exports), read as text. */
178
+ export function runtimeSources(root) {
179
+ const cached = sourceCache.get(root);
180
+ if (cached)
181
+ return cached;
182
+ const files = new Set();
183
+ try {
184
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
185
+ const collect = (value) => {
186
+ if (typeof value === "string") {
187
+ if (/\.[cm]?js$/.test(value))
188
+ files.add(value);
189
+ }
190
+ else if (value && typeof value === "object") {
191
+ for (const [key, nested] of Object.entries(value))
192
+ if (key !== "types")
193
+ collect(nested);
194
+ }
195
+ };
196
+ collect(pkg.main ?? "index.js");
197
+ collect(pkg.module);
198
+ collect(pkg.exports);
199
+ }
200
+ catch {
201
+ // No readable package.json.
202
+ }
203
+ const sources = [];
204
+ for (const file of files) {
205
+ try {
206
+ sources.push(fs.readFileSync(path.join(root, file), "utf8"));
207
+ }
208
+ catch {
209
+ // Missing entry file.
210
+ }
211
+ }
212
+ sourceCache.set(root, sources);
213
+ return sources;
214
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "realapi-check",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Catch AI-hallucinated imports, methods and options that don't exist in the package versions you actually have installed.",
5
5
  "type": "module",
6
6
  "bin": {