@doccov/sdk 0.22.0 → 0.22.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/index.d.ts +22 -3
- package/dist/index.js +130 -44
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -331,9 +331,28 @@ type DriftResult = {
|
|
|
331
331
|
exports: Map<string, SpecDocDrift[]>;
|
|
332
332
|
};
|
|
333
333
|
/**
|
|
334
|
+
* Information about an for context-aware suggestions.
|
|
335
|
+
*/
|
|
336
|
+
interface ExportInfo {
|
|
337
|
+
name: string;
|
|
338
|
+
kind: string;
|
|
339
|
+
isCallable: boolean;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Registry of exports and types for cross-reference validation.
|
|
343
|
+
*/
|
|
344
|
+
interface ExportRegistry {
|
|
345
|
+
/** Map of names to their info (for context-aware suggestions) */
|
|
346
|
+
exports: Map<string, ExportInfo>;
|
|
347
|
+
/** Set of type names (interfaces, type aliases, etc.) */
|
|
348
|
+
types: Set<string>;
|
|
349
|
+
/** Combined set of all names (for backward compatibility) */
|
|
350
|
+
all: Set<string>;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
334
353
|
* Build a registry of all export/type names for cross-reference validation.
|
|
335
354
|
*/
|
|
336
|
-
declare function buildExportRegistry(spec: OpenPkgSpec):
|
|
355
|
+
declare function buildExportRegistry(spec: OpenPkgSpec): ExportRegistry;
|
|
337
356
|
/**
|
|
338
357
|
* Compute drift for all exports in a spec.
|
|
339
358
|
*
|
|
@@ -344,10 +363,10 @@ declare function computeDrift(spec: OpenPkgSpec): DriftResult;
|
|
|
344
363
|
* Compute drift for a single export.
|
|
345
364
|
*
|
|
346
365
|
* @param entry - The to analyze
|
|
347
|
-
* @param
|
|
366
|
+
* @param registry - Registry of known exports and types for validation
|
|
348
367
|
* @returns Array of drift issues detected
|
|
349
368
|
*/
|
|
350
|
-
declare function computeExportDrift(entry: SpecExport,
|
|
369
|
+
declare function computeExportDrift(entry: SpecExport, registry?: ExportRegistry): SpecDocDrift[];
|
|
351
370
|
/**
|
|
352
371
|
* Detect runtime errors in @example blocks.
|
|
353
372
|
* Results are provided externally after running examples via runExamples().
|
package/dist/index.js
CHANGED
|
@@ -1395,27 +1395,42 @@ var LIBRARY_INTERNAL_PATTERNS = [
|
|
|
1395
1395
|
|
|
1396
1396
|
// src/analysis/docs-coverage.ts
|
|
1397
1397
|
function buildExportRegistry(spec) {
|
|
1398
|
-
const
|
|
1398
|
+
const exports = new Map;
|
|
1399
|
+
const types = new Set;
|
|
1400
|
+
const all = new Set;
|
|
1399
1401
|
for (const entry of spec.exports ?? []) {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
+
const info = {
|
|
1403
|
+
name: entry.name,
|
|
1404
|
+
kind: entry.kind ?? "unknown",
|
|
1405
|
+
isCallable: ["function", "class"].includes(entry.kind ?? "")
|
|
1406
|
+
};
|
|
1407
|
+
exports.set(entry.name, info);
|
|
1408
|
+
if (entry.id)
|
|
1409
|
+
exports.set(entry.id, info);
|
|
1410
|
+
all.add(entry.name);
|
|
1411
|
+
if (entry.id)
|
|
1412
|
+
all.add(entry.id);
|
|
1402
1413
|
}
|
|
1403
1414
|
for (const type of spec.types ?? []) {
|
|
1404
|
-
|
|
1405
|
-
|
|
1415
|
+
types.add(type.name);
|
|
1416
|
+
if (type.id)
|
|
1417
|
+
types.add(type.id);
|
|
1418
|
+
all.add(type.name);
|
|
1419
|
+
if (type.id)
|
|
1420
|
+
all.add(type.id);
|
|
1406
1421
|
}
|
|
1407
|
-
return
|
|
1422
|
+
return { exports, types, all };
|
|
1408
1423
|
}
|
|
1409
1424
|
function computeDrift(spec) {
|
|
1410
|
-
const
|
|
1425
|
+
const registry = buildExportRegistry(spec);
|
|
1411
1426
|
const exports = new Map;
|
|
1412
1427
|
for (const entry of spec.exports ?? []) {
|
|
1413
|
-
const drift = computeExportDrift(entry,
|
|
1428
|
+
const drift = computeExportDrift(entry, registry);
|
|
1414
1429
|
exports.set(entry.id ?? entry.name, drift);
|
|
1415
1430
|
}
|
|
1416
1431
|
return { exports };
|
|
1417
1432
|
}
|
|
1418
|
-
function computeExportDrift(entry,
|
|
1433
|
+
function computeExportDrift(entry, registry) {
|
|
1419
1434
|
return [
|
|
1420
1435
|
...detectParamDrift(entry),
|
|
1421
1436
|
...detectOptionalityDrift(entry),
|
|
@@ -1424,8 +1439,8 @@ function computeExportDrift(entry, exportRegistry) {
|
|
|
1424
1439
|
...detectGenericConstraintDrift(entry),
|
|
1425
1440
|
...detectDeprecatedDrift(entry),
|
|
1426
1441
|
...detectVisibilityDrift(entry),
|
|
1427
|
-
...detectExampleDrift(entry,
|
|
1428
|
-
...detectBrokenLinks(entry,
|
|
1442
|
+
...detectExampleDrift(entry, registry),
|
|
1443
|
+
...detectBrokenLinks(entry, registry),
|
|
1429
1444
|
...detectExampleSyntaxErrors(entry),
|
|
1430
1445
|
...detectAsyncMismatch(entry),
|
|
1431
1446
|
...detectPropertyTypeDrift(entry)
|
|
@@ -1472,24 +1487,39 @@ function detectParamDrift(entry) {
|
|
|
1472
1487
|
if (properties.has(firstProperty)) {
|
|
1473
1488
|
continue;
|
|
1474
1489
|
}
|
|
1475
|
-
const
|
|
1490
|
+
const propsArray = Array.from(properties);
|
|
1491
|
+
const suggestion2 = findClosestMatch(firstProperty, propsArray);
|
|
1492
|
+
let suggestionText2;
|
|
1493
|
+
if (suggestion2) {
|
|
1494
|
+
suggestionText2 = `Did you mean "${prefix}.${suggestion2.value}"?`;
|
|
1495
|
+
} else if (propsArray.length > 0 && propsArray.length <= 8) {
|
|
1496
|
+
const propsList = propsArray.slice(0, 5).map((p) => `${prefix}.${p}`);
|
|
1497
|
+
suggestionText2 = propsArray.length > 5 ? `Available: ${propsList.join(", ")}... (${propsArray.length} total)` : `Available: ${propsList.join(", ")}`;
|
|
1498
|
+
}
|
|
1476
1499
|
drifts.push({
|
|
1477
1500
|
type: "param-mismatch",
|
|
1478
1501
|
target: documentedName,
|
|
1479
1502
|
issue: `JSDoc documents property "${propertyPath}" on parameter "${prefix}" which does not exist.`,
|
|
1480
|
-
suggestion:
|
|
1503
|
+
suggestion: suggestionText2
|
|
1481
1504
|
});
|
|
1482
1505
|
continue;
|
|
1483
1506
|
}
|
|
1484
1507
|
continue;
|
|
1485
1508
|
}
|
|
1486
1509
|
}
|
|
1487
|
-
const
|
|
1510
|
+
const paramsArray = Array.from(actualParamNames);
|
|
1511
|
+
const suggestion = findClosestMatch(documentedName, paramsArray);
|
|
1512
|
+
let suggestionText;
|
|
1513
|
+
if (suggestion) {
|
|
1514
|
+
suggestionText = `Did you mean "${suggestion.value}"?`;
|
|
1515
|
+
} else if (paramsArray.length > 0 && paramsArray.length <= 6) {
|
|
1516
|
+
suggestionText = `Available parameters: ${paramsArray.join(", ")}`;
|
|
1517
|
+
}
|
|
1488
1518
|
drifts.push({
|
|
1489
1519
|
type: "param-mismatch",
|
|
1490
1520
|
target: documentedName,
|
|
1491
1521
|
issue: `JSDoc documents parameter "${documentedName}" which is not present in the signature.`,
|
|
1492
|
-
suggestion:
|
|
1522
|
+
suggestion: suggestionText
|
|
1493
1523
|
});
|
|
1494
1524
|
}
|
|
1495
1525
|
return drifts;
|
|
@@ -1960,26 +1990,53 @@ function buildGenericConstraintSuggestion(templateName, actualConstraint) {
|
|
|
1960
1990
|
}
|
|
1961
1991
|
return `Remove the constraint from @template ${templateName} to match the declaration.`;
|
|
1962
1992
|
}
|
|
1993
|
+
function splitCamelCase(str) {
|
|
1994
|
+
return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(/[\s_-]+/).filter(Boolean);
|
|
1995
|
+
}
|
|
1963
1996
|
function findClosestMatch(source, candidates) {
|
|
1964
1997
|
if (candidates.length === 0) {
|
|
1965
1998
|
return;
|
|
1966
1999
|
}
|
|
1967
|
-
const
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
return normalizedCandidate.includes(normalizedSource) || normalizedSource.includes(normalizedCandidate);
|
|
1971
|
-
});
|
|
1972
|
-
if (substringCandidate && substringCandidate !== source) {
|
|
1973
|
-
return { value: substringCandidate, distance: 0 };
|
|
1974
|
-
}
|
|
1975
|
-
let best;
|
|
2000
|
+
const sourceWords = splitCamelCase(source);
|
|
2001
|
+
let bestMatch;
|
|
2002
|
+
let bestScore = 0;
|
|
1976
2003
|
for (const candidate of candidates) {
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2004
|
+
if (candidate === source)
|
|
2005
|
+
continue;
|
|
2006
|
+
const candidateWords = splitCamelCase(candidate);
|
|
2007
|
+
let matchingWords = 0;
|
|
2008
|
+
let suffixMatch = false;
|
|
2009
|
+
if (sourceWords.length > 0 && candidateWords.length > 0) {
|
|
2010
|
+
const sourceSuffix = sourceWords[sourceWords.length - 1];
|
|
2011
|
+
const candidateSuffix = candidateWords[candidateWords.length - 1];
|
|
2012
|
+
if (sourceSuffix === candidateSuffix) {
|
|
2013
|
+
suffixMatch = true;
|
|
2014
|
+
matchingWords += 1.5;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
const suffixWord = suffixMatch ? sourceWords[sourceWords.length - 1] : null;
|
|
2018
|
+
for (const word of sourceWords) {
|
|
2019
|
+
if (word !== suffixWord && candidateWords.includes(word)) {
|
|
2020
|
+
matchingWords++;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
if (matchingWords < 2)
|
|
2024
|
+
continue;
|
|
2025
|
+
const wordScore = matchingWords / Math.max(sourceWords.length, candidateWords.length);
|
|
2026
|
+
const editDistance = levenshtein(source.toLowerCase(), candidate.toLowerCase());
|
|
2027
|
+
const maxLen = Math.max(source.length, candidate.length);
|
|
2028
|
+
const levScore = 1 - editDistance / maxLen;
|
|
2029
|
+
const totalScore = suffixMatch ? wordScore * 1.5 + levScore : wordScore + levScore * 0.5;
|
|
2030
|
+
if (totalScore > bestScore && totalScore >= 0.5) {
|
|
2031
|
+
bestScore = totalScore;
|
|
2032
|
+
bestMatch = candidate;
|
|
1980
2033
|
}
|
|
1981
2034
|
}
|
|
1982
|
-
|
|
2035
|
+
if (!bestMatch) {
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
const distance = Math.round((1 - bestScore) * 10);
|
|
2039
|
+
return { value: bestMatch, distance };
|
|
1983
2040
|
}
|
|
1984
2041
|
function levenshtein(a, b) {
|
|
1985
2042
|
if (a === b) {
|
|
@@ -2009,8 +2066,22 @@ function levenshtein(a, b) {
|
|
|
2009
2066
|
}
|
|
2010
2067
|
return matrix[b.length][a.length];
|
|
2011
2068
|
}
|
|
2012
|
-
function
|
|
2013
|
-
|
|
2069
|
+
function getIdentifierContext(node) {
|
|
2070
|
+
const parent = node.parent;
|
|
2071
|
+
if (!parent)
|
|
2072
|
+
return "value";
|
|
2073
|
+
if (ts2.isCallExpression(parent) && parent.expression === node)
|
|
2074
|
+
return "call";
|
|
2075
|
+
if (ts2.isNewExpression(parent) && parent.expression === node)
|
|
2076
|
+
return "call";
|
|
2077
|
+
if (ts2.isTypeReferenceNode(parent))
|
|
2078
|
+
return "type";
|
|
2079
|
+
if (ts2.isExpressionWithTypeArguments(parent))
|
|
2080
|
+
return "type";
|
|
2081
|
+
return "value";
|
|
2082
|
+
}
|
|
2083
|
+
function detectExampleDrift(entry, registry) {
|
|
2084
|
+
if (!registry || !entry.examples?.length)
|
|
2014
2085
|
return [];
|
|
2015
2086
|
const drifts = [];
|
|
2016
2087
|
for (const example of entry.examples) {
|
|
@@ -2024,7 +2095,11 @@ function detectExampleDrift(entry, exportRegistry) {
|
|
|
2024
2095
|
if (isLocalDeclaration(node)) {
|
|
2025
2096
|
localDeclarations.add(text);
|
|
2026
2097
|
} else if (isIdentifierReference(node) && !isBuiltInIdentifier(text)) {
|
|
2027
|
-
|
|
2098
|
+
const context = getIdentifierContext(node);
|
|
2099
|
+
const existing = referencedIdentifiers.get(text);
|
|
2100
|
+
if (!existing || context === "call") {
|
|
2101
|
+
referencedIdentifiers.set(text, context);
|
|
2102
|
+
}
|
|
2028
2103
|
}
|
|
2029
2104
|
}
|
|
2030
2105
|
ts2.forEachChild(node, visit);
|
|
@@ -2036,16 +2111,27 @@ function detectExampleDrift(entry, exportRegistry) {
|
|
|
2036
2111
|
continue;
|
|
2037
2112
|
const sourceFile = ts2.createSourceFile("example.ts", codeContent, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
|
|
2038
2113
|
const localDeclarations = new Set;
|
|
2039
|
-
const referencedIdentifiers = new
|
|
2114
|
+
const referencedIdentifiers = new Map;
|
|
2040
2115
|
visit(sourceFile);
|
|
2041
2116
|
for (const local of localDeclarations) {
|
|
2042
2117
|
referencedIdentifiers.delete(local);
|
|
2043
2118
|
}
|
|
2044
|
-
for (const identifier of referencedIdentifiers) {
|
|
2045
|
-
if (!
|
|
2046
|
-
|
|
2119
|
+
for (const [identifier, context] of referencedIdentifiers) {
|
|
2120
|
+
if (!registry.all.has(identifier)) {
|
|
2121
|
+
let candidates;
|
|
2122
|
+
if (context === "call") {
|
|
2123
|
+
candidates = Array.from(registry.exports.values()).filter((e) => e.isCallable).map((e) => e.name);
|
|
2124
|
+
} else if (context === "type") {
|
|
2125
|
+
candidates = [
|
|
2126
|
+
...Array.from(registry.types),
|
|
2127
|
+
...Array.from(registry.exports.values()).filter((e) => ["class", "interface", "type", "enum"].includes(e.kind)).map((e) => e.name)
|
|
2128
|
+
];
|
|
2129
|
+
} else {
|
|
2130
|
+
candidates = Array.from(registry.exports.keys());
|
|
2131
|
+
}
|
|
2132
|
+
const suggestion = findClosestMatch(identifier, candidates);
|
|
2047
2133
|
const isPascal = /^[A-Z]/.test(identifier);
|
|
2048
|
-
const hasCloseMatch = suggestion && suggestion.distance <=
|
|
2134
|
+
const hasCloseMatch = suggestion && suggestion.distance <= 5;
|
|
2049
2135
|
if (hasCloseMatch || isPascal) {
|
|
2050
2136
|
drifts.push({
|
|
2051
2137
|
type: "example-drift",
|
|
@@ -2087,8 +2173,8 @@ function isIdentifierReference(node) {
|
|
|
2087
2173
|
return false;
|
|
2088
2174
|
return true;
|
|
2089
2175
|
}
|
|
2090
|
-
function detectBrokenLinks(entry,
|
|
2091
|
-
if (!
|
|
2176
|
+
function detectBrokenLinks(entry, registry) {
|
|
2177
|
+
if (!registry) {
|
|
2092
2178
|
return [];
|
|
2093
2179
|
}
|
|
2094
2180
|
const drifts = [];
|
|
@@ -2115,13 +2201,13 @@ function detectBrokenLinks(entry, exportRegistry) {
|
|
|
2115
2201
|
if (target.includes("/") || target.includes("@")) {
|
|
2116
2202
|
continue;
|
|
2117
2203
|
}
|
|
2118
|
-
if (!
|
|
2119
|
-
const suggestion = findClosestMatch(rootName, Array.from(
|
|
2204
|
+
if (!registry.all.has(rootName) && !registry.all.has(target)) {
|
|
2205
|
+
const suggestion = findClosestMatch(rootName, Array.from(registry.all));
|
|
2120
2206
|
drifts.push({
|
|
2121
2207
|
type: "broken-link",
|
|
2122
2208
|
target,
|
|
2123
2209
|
issue: `{${type} ${target}} references a symbol that does not exist.`,
|
|
2124
|
-
suggestion: suggestion
|
|
2210
|
+
suggestion: suggestion ? `Did you mean "${suggestion.value}"?` : undefined
|
|
2125
2211
|
});
|
|
2126
2212
|
}
|
|
2127
2213
|
}
|
|
@@ -8532,8 +8618,8 @@ function findSimilarMember(removedName, newMembers, addedMembers) {
|
|
|
8532
8618
|
for (const name of candidates) {
|
|
8533
8619
|
if (name === removedName)
|
|
8534
8620
|
continue;
|
|
8535
|
-
const removedWords =
|
|
8536
|
-
const newWords =
|
|
8621
|
+
const removedWords = splitCamelCase2(removedName);
|
|
8622
|
+
const newWords = splitCamelCase2(name);
|
|
8537
8623
|
let matchingWords = 0;
|
|
8538
8624
|
let suffixMatch = false;
|
|
8539
8625
|
if (removedWords.length > 0 && newWords.length > 0) {
|
|
@@ -8580,7 +8666,7 @@ function levenshteinDistance(a, b) {
|
|
|
8580
8666
|
}
|
|
8581
8667
|
return matrix[b.length][a.length];
|
|
8582
8668
|
}
|
|
8583
|
-
function
|
|
8669
|
+
function splitCamelCase2(str) {
|
|
8584
8670
|
return str.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(" ");
|
|
8585
8671
|
}
|
|
8586
8672
|
|