@doccov/sdk 0.3.7 → 0.5.7
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 +48 -7
- package/dist/index.js +381 -33
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -116,7 +116,11 @@ interface ExportReference {
|
|
|
116
116
|
/**
|
|
117
117
|
* Change type for an impacted reference
|
|
118
118
|
*/
|
|
119
|
-
type DocsChangeType = "signature-changed" | "removed" | "deprecated";
|
|
119
|
+
type DocsChangeType = "signature-changed" | "removed" | "deprecated" | "method-removed" | "method-changed" | "method-deprecated";
|
|
120
|
+
/**
|
|
121
|
+
* Member-level change type
|
|
122
|
+
*/
|
|
123
|
+
type MemberChangeType = "added" | "removed" | "signature-changed";
|
|
120
124
|
/**
|
|
121
125
|
* An impacted reference in a documentation file
|
|
122
126
|
*/
|
|
@@ -131,6 +135,14 @@ interface DocsImpactReference {
|
|
|
131
135
|
suggestion?: string;
|
|
132
136
|
/** Context around the reference */
|
|
133
137
|
context?: string;
|
|
138
|
+
/** Member/method name if this is a member-level change */
|
|
139
|
+
memberName?: string;
|
|
140
|
+
/** Type of member change (added, removed, signature-changed) */
|
|
141
|
+
memberChangeType?: MemberChangeType;
|
|
142
|
+
/** Suggested replacement for removed/changed members */
|
|
143
|
+
replacementSuggestion?: string;
|
|
144
|
+
/** True if this is just a class instantiation (new ClassName()) */
|
|
145
|
+
isInstantiation?: boolean;
|
|
134
146
|
}
|
|
135
147
|
/**
|
|
136
148
|
* Documentation file impact summary
|
|
@@ -147,8 +159,10 @@ interface DocsImpact {
|
|
|
147
159
|
interface DocsImpactResult {
|
|
148
160
|
/** Files with impacted references */
|
|
149
161
|
impactedFiles: DocsImpact[];
|
|
150
|
-
/** New exports that have no documentation */
|
|
162
|
+
/** New exports (from this diff) that have no documentation */
|
|
151
163
|
missingDocs: string[];
|
|
164
|
+
/** ALL exports from the spec that have no documentation in the scanned files */
|
|
165
|
+
allUndocumented: string[];
|
|
152
166
|
/** Statistics */
|
|
153
167
|
stats: {
|
|
154
168
|
/** Total markdown files scanned */
|
|
@@ -159,6 +173,10 @@ interface DocsImpactResult {
|
|
|
159
173
|
referencesFound: number;
|
|
160
174
|
/** References impacted by changes */
|
|
161
175
|
impactedReferences: number;
|
|
176
|
+
/** Total exports in the spec */
|
|
177
|
+
totalExports: number;
|
|
178
|
+
/** Exports found documented in markdown */
|
|
179
|
+
documentedExports: number;
|
|
162
180
|
};
|
|
163
181
|
}
|
|
164
182
|
/**
|
|
@@ -198,14 +216,32 @@ declare function findExportReferences(files: MarkdownDocFile[], exportNames: str
|
|
|
198
216
|
*/
|
|
199
217
|
declare function blockReferencesExport(block: MarkdownCodeBlock, exportName: string): boolean;
|
|
200
218
|
import { SpecDiff } from "@openpkg-ts/spec";
|
|
219
|
+
type MemberChangeType2 = "added" | "removed" | "signature-changed";
|
|
220
|
+
interface MemberChange {
|
|
221
|
+
/** The class this member belongs to */
|
|
222
|
+
className: string;
|
|
223
|
+
/** The member name (e.g., "evaluateChainhook") */
|
|
224
|
+
memberName: string;
|
|
225
|
+
/** Kind of member */
|
|
226
|
+
memberKind: "method" | "property" | "accessor" | "constructor";
|
|
227
|
+
/** Type of change */
|
|
228
|
+
changeType: MemberChangeType2;
|
|
229
|
+
/** Old signature string (for signature changes) */
|
|
230
|
+
oldSignature?: string;
|
|
231
|
+
/** New signature string (for signature changes) */
|
|
232
|
+
newSignature?: string;
|
|
233
|
+
/** Suggested replacement (e.g., "Use replayChainhook instead") */
|
|
234
|
+
suggestion?: string;
|
|
235
|
+
}
|
|
201
236
|
/**
|
|
202
237
|
* Analyze docs impact from a spec diff
|
|
203
238
|
*
|
|
204
239
|
* @param diff - The spec diff result
|
|
205
240
|
* @param markdownFiles - Parsed markdown files
|
|
206
241
|
* @param newExportNames - All names in the new spec (for missing docs detection)
|
|
242
|
+
* @param memberChanges - Optional member-level changes for granular detection
|
|
207
243
|
*/
|
|
208
|
-
declare function analyzeDocsImpact(diff: SpecDiff, markdownFiles: MarkdownDocFile[], newExportNames?: string[]): DocsImpactResult;
|
|
244
|
+
declare function analyzeDocsImpact(diff: SpecDiff, markdownFiles: MarkdownDocFile[], newExportNames?: string[], memberChanges?: MemberChange[]): DocsImpactResult;
|
|
209
245
|
/**
|
|
210
246
|
* Find references to deprecated exports
|
|
211
247
|
*/
|
|
@@ -225,13 +261,17 @@ declare function getDocumentedExports(markdownFiles: MarkdownDocFile[], exportNa
|
|
|
225
261
|
* Get all exports that lack documentation
|
|
226
262
|
*/
|
|
227
263
|
declare function getUndocumentedExports(markdownFiles: MarkdownDocFile[], exportNames: string[]): string[];
|
|
228
|
-
import { OpenPkg as
|
|
264
|
+
import { CategorizedBreaking, OpenPkg as OpenPkg3, SpecDiff as SpecDiff2 } from "@openpkg-ts/spec";
|
|
229
265
|
/**
|
|
230
266
|
* Extended spec diff result with docs impact
|
|
231
267
|
*/
|
|
232
268
|
interface SpecDiffWithDocs extends SpecDiff2 {
|
|
233
269
|
/** Docs impact analysis (only present if markdown files provided) */
|
|
234
270
|
docsImpact?: DocsImpactResult;
|
|
271
|
+
/** Member-level changes for classes (methods added/removed/changed) */
|
|
272
|
+
memberChanges?: MemberChange[];
|
|
273
|
+
/** Breaking changes categorized by severity (high/medium/low) */
|
|
274
|
+
categorizedBreaking?: CategorizedBreaking[];
|
|
235
275
|
}
|
|
236
276
|
/**
|
|
237
277
|
* Options for diffSpecWithDocs
|
|
@@ -263,7 +303,7 @@ interface DiffWithDocsOptions {
|
|
|
263
303
|
* }
|
|
264
304
|
* ```
|
|
265
305
|
*/
|
|
266
|
-
declare function diffSpecWithDocs(oldSpec:
|
|
306
|
+
declare function diffSpecWithDocs(oldSpec: OpenPkg3, newSpec: OpenPkg3, options?: DiffWithDocsOptions): SpecDiffWithDocs;
|
|
267
307
|
/**
|
|
268
308
|
* Check if a diff has any docs impact
|
|
269
309
|
*/
|
|
@@ -276,6 +316,7 @@ declare function getDocsImpactSummary(diff: SpecDiffWithDocs): {
|
|
|
276
316
|
impactedReferenceCount: number;
|
|
277
317
|
missingDocsCount: number;
|
|
278
318
|
totalIssues: number;
|
|
319
|
+
memberChangesCount: number;
|
|
279
320
|
};
|
|
280
321
|
interface DocCovOptions {
|
|
281
322
|
includePrivate?: boolean;
|
|
@@ -466,7 +507,7 @@ declare class DocCov {
|
|
|
466
507
|
declare function analyze(code: string, options?: AnalyzeOptions): Promise<OpenPkgSpec>;
|
|
467
508
|
declare function analyzeFile(filePath: string, options?: AnalyzeOptions): Promise<OpenPkgSpec>;
|
|
468
509
|
/** @deprecated Use DocCov instead */
|
|
469
|
-
declare const
|
|
510
|
+
declare const OpenPkg4: typeof DocCov;
|
|
470
511
|
/**
|
|
471
512
|
* Project detection types for I/O-agnostic project analysis.
|
|
472
513
|
* Used by both CLI (NodeFileSystem) and API (SandboxFileSystem).
|
|
@@ -723,4 +764,4 @@ declare function readPackageJson(fs: FileSystem, dir: string): Promise<PackageJs
|
|
|
723
764
|
* ```
|
|
724
765
|
*/
|
|
725
766
|
declare function analyzeProject2(fs: FileSystem, options?: AnalyzeProjectOptions): Promise<ProjectInfo>;
|
|
726
|
-
export { serializeJSDoc, safeParseJson, runExamplesWithPackage, runExamples, runExample, readPackageJson, parseMarkdownFiles, parseMarkdownFile, parseJSDocToPatch, parseAssertions, mergeFixes, isFixableDrift, isExecutableLang, hasNonAssertionComments, hasDocsImpact, hasDocsForExport, getUndocumentedExports, getRunCommand, getPrimaryBuildScript, getInstallCommand, getDocumentedExports, getDocsImpactSummary, generateFixesForExport, generateFix, formatPackageList, findRemovedReferences, findPackageByName, findJSDocLocation, findExportReferences, findDeprecatedReferences, extractPackageSpec, extractImports, extractFunctionCalls, diffSpecWithDocs, detectPackageManager, detectMonorepo, detectExampleRuntimeErrors, detectExampleAssertionFailures, detectEntryPoint, detectBuildInfo, createSourceFile, categorizeDrifts, blockReferencesExport, applyPatchToJSDoc, applyEdits, analyzeProject2 as analyzeProject, analyzeFile, analyzeDocsImpact, analyze, WorkspacePackage, SpecDiffWithDocs, SandboxFileSystem, RunExamplesWithPackageResult, RunExamplesWithPackageOptions, RunExampleOptions, ProjectInfo, PackageManagerInfo, PackageManager, PackageJson, PackageExports, OpenPkgSpec, OpenPkgOptions,
|
|
767
|
+
export { serializeJSDoc, safeParseJson, runExamplesWithPackage, runExamples, runExample, readPackageJson, parseMarkdownFiles, parseMarkdownFile, parseJSDocToPatch, parseAssertions, mergeFixes, isFixableDrift, isExecutableLang, hasNonAssertionComments, hasDocsImpact, hasDocsForExport, getUndocumentedExports, getRunCommand, getPrimaryBuildScript, getInstallCommand, getDocumentedExports, getDocsImpactSummary, generateFixesForExport, generateFix, formatPackageList, findRemovedReferences, findPackageByName, findJSDocLocation, findExportReferences, findDeprecatedReferences, extractPackageSpec, extractImports, extractFunctionCalls, diffSpecWithDocs, detectPackageManager, detectMonorepo, detectExampleRuntimeErrors, detectExampleAssertionFailures, detectEntryPoint, detectBuildInfo, createSourceFile, categorizeDrifts, blockReferencesExport, applyPatchToJSDoc, applyEdits, analyzeProject2 as analyzeProject, analyzeFile, analyzeDocsImpact, analyze, WorkspacePackage, SpecDiffWithDocs, SandboxFileSystem, RunExamplesWithPackageResult, RunExamplesWithPackageOptions, RunExampleOptions, ProjectInfo, PackageManagerInfo, PackageManager, PackageJson, PackageExports, OpenPkgSpec, OpenPkgOptions, OpenPkg4 as OpenPkg, NodeFileSystem, MonorepoType, MonorepoInfo, MemberChange, MarkdownDocFile, MarkdownCodeBlock, JSDocTag, JSDocReturn, JSDocPatch, JSDocParam, JSDocEdit, FixType, FixSuggestion, FilterOptions, FileSystem, ExportReference, ExampleRunResult, EntryPointSource, EntryPointInfo, DocsImpactResult, DocsImpactReference, DocsImpact, DocsChangeType, DocCovOptions, DocCov, DiffWithDocsOptions, Diagnostic, BuildInfo, ApplyEditsResult, AnalyzeProjectOptions, AnalyzeOptions, AnalysisResult };
|
package/dist/index.js
CHANGED
|
@@ -1107,6 +1107,39 @@ function extractFunctionCalls(code) {
|
|
|
1107
1107
|
}
|
|
1108
1108
|
return Array.from(calls);
|
|
1109
1109
|
}
|
|
1110
|
+
function extractMethodCalls(code) {
|
|
1111
|
+
const calls = [];
|
|
1112
|
+
const lines = code.split(`
|
|
1113
|
+
`);
|
|
1114
|
+
const methodCallRegex = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
1115
|
+
const skipObjects = new Set(["console", "Math", "JSON", "Object", "Array", "String", "Number"]);
|
|
1116
|
+
for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) {
|
|
1117
|
+
const line = lines[lineIndex];
|
|
1118
|
+
let match;
|
|
1119
|
+
methodCallRegex.lastIndex = 0;
|
|
1120
|
+
while ((match = methodCallRegex.exec(line)) !== null) {
|
|
1121
|
+
const objectName = match[1];
|
|
1122
|
+
const methodName = match[2];
|
|
1123
|
+
if (skipObjects.has(objectName)) {
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
calls.push({
|
|
1127
|
+
objectName,
|
|
1128
|
+
methodName,
|
|
1129
|
+
line: lineIndex,
|
|
1130
|
+
context: line.trim()
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return calls;
|
|
1135
|
+
}
|
|
1136
|
+
function hasInstantiation(code, className) {
|
|
1137
|
+
const regex = new RegExp(`\\bnew\\s+${escapeRegex(className)}\\s*\\(`, "g");
|
|
1138
|
+
return regex.test(code);
|
|
1139
|
+
}
|
|
1140
|
+
function escapeRegex(str) {
|
|
1141
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1142
|
+
}
|
|
1110
1143
|
function findExportReferences(files, exportNames) {
|
|
1111
1144
|
const references = [];
|
|
1112
1145
|
const exportSet = new Set(exportNames);
|
|
@@ -1168,58 +1201,138 @@ function blockReferencesExport(block, exportName) {
|
|
|
1168
1201
|
return calls.includes(exportName);
|
|
1169
1202
|
}
|
|
1170
1203
|
// src/markdown/analyzer.ts
|
|
1171
|
-
function getChangeType(exportName, diff) {
|
|
1204
|
+
function getChangeType(exportName, diff, newExportNames) {
|
|
1172
1205
|
if (diff.breaking.includes(exportName)) {
|
|
1206
|
+
if (!newExportNames.includes(exportName)) {
|
|
1207
|
+
return "removed";
|
|
1208
|
+
}
|
|
1173
1209
|
return "signature-changed";
|
|
1174
1210
|
}
|
|
1175
1211
|
return null;
|
|
1176
1212
|
}
|
|
1177
|
-
function
|
|
1213
|
+
function mapMemberChangeType(memberChangeType) {
|
|
1214
|
+
switch (memberChangeType) {
|
|
1215
|
+
case "removed":
|
|
1216
|
+
return "method-removed";
|
|
1217
|
+
case "signature-changed":
|
|
1218
|
+
return "method-changed";
|
|
1219
|
+
default:
|
|
1220
|
+
return "signature-changed";
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
function analyzeDocsImpact(diff, markdownFiles, newExportNames = [], memberChanges) {
|
|
1178
1224
|
const changedExports = [
|
|
1179
1225
|
...diff.breaking
|
|
1180
1226
|
];
|
|
1181
|
-
const
|
|
1227
|
+
const memberChangesByMethod = new Map;
|
|
1228
|
+
if (memberChanges) {
|
|
1229
|
+
for (const mc of memberChanges) {
|
|
1230
|
+
memberChangesByMethod.set(mc.memberName, mc);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const classesWithMemberChanges = new Set(memberChanges?.map((mc) => mc.className) ?? []);
|
|
1182
1234
|
const impactByFile = new Map;
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
if (!changeType)
|
|
1186
|
-
continue;
|
|
1187
|
-
let impact = impactByFile.get(ref.file);
|
|
1235
|
+
const addReference = (file, ref) => {
|
|
1236
|
+
let impact = impactByFile.get(file);
|
|
1188
1237
|
if (!impact) {
|
|
1189
|
-
impact = { file
|
|
1190
|
-
impactByFile.set(
|
|
1191
|
-
}
|
|
1192
|
-
impact.references.push(
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1238
|
+
impact = { file, references: [] };
|
|
1239
|
+
impactByFile.set(file, impact);
|
|
1240
|
+
}
|
|
1241
|
+
impact.references.push(ref);
|
|
1242
|
+
};
|
|
1243
|
+
for (const mdFile of markdownFiles) {
|
|
1244
|
+
for (const block of mdFile.codeBlocks) {
|
|
1245
|
+
const reportedRefs = new Set;
|
|
1246
|
+
if (memberChanges && memberChanges.length > 0) {
|
|
1247
|
+
const methodCalls = extractMethodCalls(block.code);
|
|
1248
|
+
for (const call of methodCalls) {
|
|
1249
|
+
const memberChange = memberChangesByMethod.get(call.methodName);
|
|
1250
|
+
if (memberChange) {
|
|
1251
|
+
const refKey = `${mdFile.path}:${block.lineStart + call.line}:${call.methodName}`;
|
|
1252
|
+
if (!reportedRefs.has(refKey)) {
|
|
1253
|
+
reportedRefs.add(refKey);
|
|
1254
|
+
addReference(mdFile.path, {
|
|
1255
|
+
exportName: memberChange.className,
|
|
1256
|
+
memberName: call.methodName,
|
|
1257
|
+
memberChangeType: memberChange.changeType,
|
|
1258
|
+
changeType: mapMemberChangeType(memberChange.changeType),
|
|
1259
|
+
replacementSuggestion: memberChange.suggestion,
|
|
1260
|
+
line: block.lineStart + call.line,
|
|
1261
|
+
context: call.context,
|
|
1262
|
+
isInstantiation: false
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
for (const className of classesWithMemberChanges) {
|
|
1268
|
+
if (hasInstantiation(block.code, className)) {
|
|
1269
|
+
const refKey = `${mdFile.path}:${block.lineStart}:new ${className}`;
|
|
1270
|
+
if (!reportedRefs.has(refKey)) {
|
|
1271
|
+
const hasMethodRefs = Array.from(reportedRefs).some((key) => key.startsWith(`${mdFile.path}:`) && memberChanges.some((mc) => mc.className === className && key.includes(mc.memberName)));
|
|
1272
|
+
if (!hasMethodRefs) {
|
|
1273
|
+
reportedRefs.add(refKey);
|
|
1274
|
+
addReference(mdFile.path, {
|
|
1275
|
+
exportName: className,
|
|
1276
|
+
line: block.lineStart,
|
|
1277
|
+
changeType: "signature-changed",
|
|
1278
|
+
context: `new ${className}(...)`,
|
|
1279
|
+
isInstantiation: true
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
const exportsWithoutMemberChanges = changedExports.filter((name) => !classesWithMemberChanges.has(name));
|
|
1287
|
+
if (exportsWithoutMemberChanges.length > 0) {
|
|
1288
|
+
const refs = findExportReferences([{ ...mdFile, codeBlocks: [block] }], exportsWithoutMemberChanges);
|
|
1289
|
+
for (const ref of refs) {
|
|
1290
|
+
const changeType = getChangeType(ref.exportName, diff, newExportNames);
|
|
1291
|
+
if (!changeType)
|
|
1292
|
+
continue;
|
|
1293
|
+
const refKey = `${ref.file}:${ref.line}:${ref.exportName}`;
|
|
1294
|
+
if (!reportedRefs.has(refKey)) {
|
|
1295
|
+
reportedRefs.add(refKey);
|
|
1296
|
+
addReference(ref.file, {
|
|
1297
|
+
exportName: ref.exportName,
|
|
1298
|
+
line: ref.line,
|
|
1299
|
+
changeType,
|
|
1300
|
+
context: ref.context
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1198
1306
|
}
|
|
1199
|
-
const
|
|
1307
|
+
const documentedExportsSet = new Set;
|
|
1200
1308
|
for (const file of markdownFiles) {
|
|
1201
1309
|
for (const block of file.codeBlocks) {
|
|
1202
1310
|
for (const exportName of newExportNames) {
|
|
1203
1311
|
if (block.code.includes(exportName)) {
|
|
1204
|
-
|
|
1312
|
+
documentedExportsSet.add(exportName);
|
|
1205
1313
|
}
|
|
1206
1314
|
}
|
|
1207
1315
|
}
|
|
1208
1316
|
}
|
|
1209
|
-
const missingDocs = diff.nonBreaking.filter((name) => !
|
|
1317
|
+
const missingDocs = diff.nonBreaking.filter((name) => !documentedExportsSet.has(name));
|
|
1318
|
+
const allUndocumented = newExportNames.filter((name) => !documentedExportsSet.has(name));
|
|
1210
1319
|
const totalCodeBlocks = markdownFiles.reduce((sum, f) => sum + f.codeBlocks.length, 0);
|
|
1211
|
-
const allReferences = findExportReferences(markdownFiles, [
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1320
|
+
const allReferences = findExportReferences(markdownFiles, [...changedExports, ...diff.nonBreaking]);
|
|
1321
|
+
let impactedReferences = 0;
|
|
1322
|
+
for (const impact of impactByFile.values()) {
|
|
1323
|
+
impactedReferences += impact.references.length;
|
|
1324
|
+
}
|
|
1215
1325
|
return {
|
|
1216
1326
|
impactedFiles: Array.from(impactByFile.values()),
|
|
1217
1327
|
missingDocs,
|
|
1328
|
+
allUndocumented,
|
|
1218
1329
|
stats: {
|
|
1219
1330
|
filesScanned: markdownFiles.length,
|
|
1220
1331
|
codeBlocksFound: totalCodeBlocks,
|
|
1221
1332
|
referencesFound: allReferences.length,
|
|
1222
|
-
impactedReferences
|
|
1333
|
+
impactedReferences,
|
|
1334
|
+
totalExports: newExportNames.length,
|
|
1335
|
+
documentedExports: documentedExportsSet.size
|
|
1223
1336
|
}
|
|
1224
1337
|
};
|
|
1225
1338
|
}
|
|
@@ -1246,18 +1359,251 @@ function getUndocumentedExports(markdownFiles, exportNames) {
|
|
|
1246
1359
|
const documented = new Set(getDocumentedExports(markdownFiles, exportNames));
|
|
1247
1360
|
return exportNames.filter((name) => !documented.has(name));
|
|
1248
1361
|
}
|
|
1362
|
+
// src/markdown/member-diff.ts
|
|
1363
|
+
function diffMemberChanges(oldSpec, newSpec, changedClassNames) {
|
|
1364
|
+
const changes = [];
|
|
1365
|
+
const oldExportMap = toExportMap(oldSpec.exports ?? []);
|
|
1366
|
+
const newExportMap = toExportMap(newSpec.exports ?? []);
|
|
1367
|
+
for (const className of changedClassNames) {
|
|
1368
|
+
const oldExport = oldExportMap.get(className);
|
|
1369
|
+
const newExport = newExportMap.get(className);
|
|
1370
|
+
if (!oldExport?.members && !newExport?.members) {
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
const oldMembers = toMemberMap(oldExport?.members ?? []);
|
|
1374
|
+
const newMembers = toMemberMap(newExport?.members ?? []);
|
|
1375
|
+
const addedMemberNames = [];
|
|
1376
|
+
for (const [memberName, newMember] of newMembers) {
|
|
1377
|
+
if (!oldMembers.has(memberName)) {
|
|
1378
|
+
addedMemberNames.push(memberName);
|
|
1379
|
+
changes.push({
|
|
1380
|
+
className,
|
|
1381
|
+
memberName,
|
|
1382
|
+
memberKind: getMemberKind(newMember),
|
|
1383
|
+
changeType: "added",
|
|
1384
|
+
newSignature: formatSignature(newMember)
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
for (const [memberName, oldMember] of oldMembers) {
|
|
1389
|
+
if (!newMembers.has(memberName)) {
|
|
1390
|
+
const suggestion = findSimilarMember(memberName, newMembers, addedMemberNames);
|
|
1391
|
+
changes.push({
|
|
1392
|
+
className,
|
|
1393
|
+
memberName,
|
|
1394
|
+
memberKind: getMemberKind(oldMember),
|
|
1395
|
+
changeType: "removed",
|
|
1396
|
+
oldSignature: formatSignature(oldMember),
|
|
1397
|
+
suggestion
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
for (const [memberName, oldMember] of oldMembers) {
|
|
1402
|
+
const newMember = newMembers.get(memberName);
|
|
1403
|
+
if (newMember && hasSignatureChanged(oldMember, newMember)) {
|
|
1404
|
+
changes.push({
|
|
1405
|
+
className,
|
|
1406
|
+
memberName,
|
|
1407
|
+
memberKind: getMemberKind(newMember),
|
|
1408
|
+
changeType: "signature-changed",
|
|
1409
|
+
oldSignature: formatSignature(oldMember),
|
|
1410
|
+
newSignature: formatSignature(newMember)
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
return deduplicateMemberChanges(changes);
|
|
1416
|
+
}
|
|
1417
|
+
function deduplicateMemberChanges(changes) {
|
|
1418
|
+
const seen = new Set;
|
|
1419
|
+
return changes.filter((mc) => {
|
|
1420
|
+
const key = `${mc.className}:${mc.memberName}:${mc.changeType}`;
|
|
1421
|
+
if (seen.has(key))
|
|
1422
|
+
return false;
|
|
1423
|
+
seen.add(key);
|
|
1424
|
+
return true;
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
function toExportMap(exports) {
|
|
1428
|
+
const map = new Map;
|
|
1429
|
+
for (const exp of exports) {
|
|
1430
|
+
if (exp?.name) {
|
|
1431
|
+
map.set(exp.name, exp);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return map;
|
|
1435
|
+
}
|
|
1436
|
+
function toMemberMap(members) {
|
|
1437
|
+
const map = new Map;
|
|
1438
|
+
for (const member of members) {
|
|
1439
|
+
if (member?.name) {
|
|
1440
|
+
map.set(member.name, member);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return map;
|
|
1444
|
+
}
|
|
1445
|
+
function getMemberKind(member) {
|
|
1446
|
+
switch (member.kind) {
|
|
1447
|
+
case "method":
|
|
1448
|
+
return "method";
|
|
1449
|
+
case "property":
|
|
1450
|
+
return "property";
|
|
1451
|
+
case "accessor":
|
|
1452
|
+
return "accessor";
|
|
1453
|
+
case "constructor":
|
|
1454
|
+
return "constructor";
|
|
1455
|
+
default:
|
|
1456
|
+
return "method";
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
function formatSignature(member) {
|
|
1460
|
+
if (!member.signatures?.length) {
|
|
1461
|
+
return member.name ?? "";
|
|
1462
|
+
}
|
|
1463
|
+
const sig = member.signatures[0];
|
|
1464
|
+
const params = sig.parameters?.map((p) => {
|
|
1465
|
+
const optional = p.required === false ? "?" : "";
|
|
1466
|
+
const typeName = extractTypeName(p.schema);
|
|
1467
|
+
return typeName ? `${p.name}${optional}: ${typeName}` : `${p.name}${optional}`;
|
|
1468
|
+
}) ?? [];
|
|
1469
|
+
return `${member.name}(${params.join(", ")})`;
|
|
1470
|
+
}
|
|
1471
|
+
function extractTypeName(schema) {
|
|
1472
|
+
if (!schema || typeof schema !== "object") {
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
const s = schema;
|
|
1476
|
+
if (typeof s.$ref === "string") {
|
|
1477
|
+
const parts = s.$ref.split("/");
|
|
1478
|
+
return parts[parts.length - 1];
|
|
1479
|
+
}
|
|
1480
|
+
if (typeof s.type === "string") {
|
|
1481
|
+
return s.type;
|
|
1482
|
+
}
|
|
1483
|
+
if (typeof s.tsType === "string") {
|
|
1484
|
+
const tsType = s.tsType;
|
|
1485
|
+
if (tsType.length > 30) {
|
|
1486
|
+
return tsType.slice(0, 27) + "...";
|
|
1487
|
+
}
|
|
1488
|
+
return tsType;
|
|
1489
|
+
}
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
function hasSignatureChanged(oldMember, newMember) {
|
|
1493
|
+
const oldSigs = oldMember.signatures ?? [];
|
|
1494
|
+
const newSigs = newMember.signatures ?? [];
|
|
1495
|
+
if (oldSigs.length !== newSigs.length) {
|
|
1496
|
+
return true;
|
|
1497
|
+
}
|
|
1498
|
+
for (let i = 0;i < oldSigs.length; i++) {
|
|
1499
|
+
if (!signaturesEqual(oldSigs[i], newSigs[i])) {
|
|
1500
|
+
return true;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
function signaturesEqual(a, b) {
|
|
1506
|
+
const aParams = a.parameters ?? [];
|
|
1507
|
+
const bParams = b.parameters ?? [];
|
|
1508
|
+
if (aParams.length !== bParams.length) {
|
|
1509
|
+
return false;
|
|
1510
|
+
}
|
|
1511
|
+
for (let i = 0;i < aParams.length; i++) {
|
|
1512
|
+
const ap = aParams[i];
|
|
1513
|
+
const bp = bParams[i];
|
|
1514
|
+
if (ap.name !== bp.name)
|
|
1515
|
+
return false;
|
|
1516
|
+
if (ap.required !== bp.required)
|
|
1517
|
+
return false;
|
|
1518
|
+
if (JSON.stringify(ap.schema) !== JSON.stringify(bp.schema))
|
|
1519
|
+
return false;
|
|
1520
|
+
}
|
|
1521
|
+
if (JSON.stringify(a.returns) !== JSON.stringify(b.returns)) {
|
|
1522
|
+
return false;
|
|
1523
|
+
}
|
|
1524
|
+
return true;
|
|
1525
|
+
}
|
|
1526
|
+
function findSimilarMember(removedName, newMembers, addedMembers) {
|
|
1527
|
+
const candidates = addedMembers.length > 0 ? addedMembers : Array.from(newMembers.keys());
|
|
1528
|
+
let bestMatch;
|
|
1529
|
+
let bestScore = 0;
|
|
1530
|
+
for (const name of candidates) {
|
|
1531
|
+
if (name === removedName)
|
|
1532
|
+
continue;
|
|
1533
|
+
const removedWords = splitCamelCase(removedName);
|
|
1534
|
+
const newWords = splitCamelCase(name);
|
|
1535
|
+
let matchingWords = 0;
|
|
1536
|
+
let suffixMatch = false;
|
|
1537
|
+
if (removedWords.length > 0 && newWords.length > 0) {
|
|
1538
|
+
const removedSuffix = removedWords[removedWords.length - 1];
|
|
1539
|
+
const newSuffix = newWords[newWords.length - 1];
|
|
1540
|
+
if (removedSuffix === newSuffix) {
|
|
1541
|
+
suffixMatch = true;
|
|
1542
|
+
matchingWords += 2;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
for (const word of removedWords) {
|
|
1546
|
+
if (newWords.includes(word)) {
|
|
1547
|
+
matchingWords++;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
const wordScore = matchingWords / Math.max(removedWords.length, newWords.length);
|
|
1551
|
+
const editDistance = levenshteinDistance(removedName.toLowerCase(), name.toLowerCase());
|
|
1552
|
+
const maxLen = Math.max(removedName.length, name.length);
|
|
1553
|
+
const levenScore = 1 - editDistance / maxLen;
|
|
1554
|
+
const totalScore = suffixMatch ? wordScore * 1.5 + levenScore : wordScore + levenScore * 0.5;
|
|
1555
|
+
if (totalScore > bestScore && totalScore >= 0.5) {
|
|
1556
|
+
bestScore = totalScore;
|
|
1557
|
+
bestMatch = name;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return bestMatch ? `Use ${bestMatch} instead` : undefined;
|
|
1561
|
+
}
|
|
1562
|
+
function levenshteinDistance(a, b) {
|
|
1563
|
+
const matrix = [];
|
|
1564
|
+
for (let i = 0;i <= b.length; i++) {
|
|
1565
|
+
matrix[i] = [i];
|
|
1566
|
+
}
|
|
1567
|
+
for (let j = 0;j <= a.length; j++) {
|
|
1568
|
+
matrix[0][j] = j;
|
|
1569
|
+
}
|
|
1570
|
+
for (let i = 1;i <= b.length; i++) {
|
|
1571
|
+
for (let j = 1;j <= a.length; j++) {
|
|
1572
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
1573
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
1574
|
+
} else {
|
|
1575
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return matrix[b.length][a.length];
|
|
1580
|
+
}
|
|
1581
|
+
function splitCamelCase(str) {
|
|
1582
|
+
return str.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(" ");
|
|
1583
|
+
}
|
|
1249
1584
|
// src/markdown/diff-with-docs.ts
|
|
1250
|
-
import {
|
|
1585
|
+
import {
|
|
1586
|
+
categorizeBreakingChanges,
|
|
1587
|
+
diffSpec
|
|
1588
|
+
} from "@openpkg-ts/spec";
|
|
1251
1589
|
function diffSpecWithDocs(oldSpec, newSpec, options = {}) {
|
|
1252
1590
|
const baseDiff = diffSpec(oldSpec, newSpec);
|
|
1591
|
+
const memberChanges = diffMemberChanges(oldSpec, newSpec, baseDiff.breaking);
|
|
1592
|
+
const categorizedBreaking = categorizeBreakingChanges(baseDiff.breaking, oldSpec, newSpec, memberChanges);
|
|
1253
1593
|
if (!options.markdownFiles?.length) {
|
|
1254
|
-
return
|
|
1594
|
+
return {
|
|
1595
|
+
...baseDiff,
|
|
1596
|
+
memberChanges: memberChanges.length > 0 ? memberChanges : undefined,
|
|
1597
|
+
categorizedBreaking: categorizedBreaking.length > 0 ? categorizedBreaking : undefined
|
|
1598
|
+
};
|
|
1255
1599
|
}
|
|
1256
1600
|
const newExportNames = newSpec.exports?.map((e) => e.name) ?? [];
|
|
1257
|
-
const docsImpact = analyzeDocsImpact(baseDiff, options.markdownFiles, newExportNames);
|
|
1601
|
+
const docsImpact = analyzeDocsImpact(baseDiff, options.markdownFiles, newExportNames, memberChanges);
|
|
1258
1602
|
return {
|
|
1259
1603
|
...baseDiff,
|
|
1260
|
-
docsImpact
|
|
1604
|
+
docsImpact,
|
|
1605
|
+
memberChanges: memberChanges.length > 0 ? memberChanges : undefined,
|
|
1606
|
+
categorizedBreaking: categorizedBreaking.length > 0 ? categorizedBreaking : undefined
|
|
1261
1607
|
};
|
|
1262
1608
|
}
|
|
1263
1609
|
function hasDocsImpact(diff) {
|
|
@@ -1271,7 +1617,8 @@ function getDocsImpactSummary(diff) {
|
|
|
1271
1617
|
impactedFileCount: 0,
|
|
1272
1618
|
impactedReferenceCount: 0,
|
|
1273
1619
|
missingDocsCount: 0,
|
|
1274
|
-
totalIssues: 0
|
|
1620
|
+
totalIssues: 0,
|
|
1621
|
+
memberChangesCount: diff.memberChanges?.length ?? 0
|
|
1275
1622
|
};
|
|
1276
1623
|
}
|
|
1277
1624
|
const impactedFileCount = diff.docsImpact.impactedFiles.length;
|
|
@@ -1281,7 +1628,8 @@ function getDocsImpactSummary(diff) {
|
|
|
1281
1628
|
impactedFileCount,
|
|
1282
1629
|
impactedReferenceCount,
|
|
1283
1630
|
missingDocsCount,
|
|
1284
|
-
totalIssues: impactedReferenceCount + missingDocsCount
|
|
1631
|
+
totalIssues: impactedReferenceCount + missingDocsCount,
|
|
1632
|
+
memberChangesCount: diff.memberChanges?.length ?? 0
|
|
1285
1633
|
};
|
|
1286
1634
|
}
|
|
1287
1635
|
// src/analysis/run-analysis.ts
|
|
@@ -4122,7 +4470,7 @@ function generateAssertionFix(drift, exportEntry) {
|
|
|
4122
4470
|
const oldValue = oldValueMatch?.[1];
|
|
4123
4471
|
if (!oldValue)
|
|
4124
4472
|
return null;
|
|
4125
|
-
const updatedExample = oldExample.replace(new RegExp(`//\\s*=>\\s*${
|
|
4473
|
+
const updatedExample = oldExample.replace(new RegExp(`//\\s*=>\\s*${escapeRegex2(oldValue)}`, "g"), `// => ${newValue}`);
|
|
4126
4474
|
const updatedExamples = [...examples];
|
|
4127
4475
|
updatedExamples[exampleIndex] = updatedExample;
|
|
4128
4476
|
return {
|
|
@@ -4240,7 +4588,7 @@ function stringifySchema(schema) {
|
|
|
4240
4588
|
}
|
|
4241
4589
|
return "unknown";
|
|
4242
4590
|
}
|
|
4243
|
-
function
|
|
4591
|
+
function escapeRegex2(str) {
|
|
4244
4592
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4245
4593
|
}
|
|
4246
4594
|
function categorizeDrifts(drifts) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doccov/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "DocCov SDK - Documentation coverage and drift detection for TypeScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dist"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@openpkg-ts/spec": "^0.
|
|
42
|
+
"@openpkg-ts/spec": "^0.4.0",
|
|
43
43
|
"@vercel/sandbox": "^1.0.3",
|
|
44
44
|
"mdast": "^3.0.0",
|
|
45
45
|
"remark-mdx": "^3.1.0",
|