@aiready/doc-drift 0.13.4 → 0.13.5
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/.turbo/turbo-build.log +10 -10
- package/dist/{chunk-5BGWZWHD.mjs → chunk-P74XAVQ3.mjs} +21 -6
- package/dist/cli.js +21 -6
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +39 -17
- package/dist/index.mjs +19 -12
- package/package.json +2 -2
- package/src/__tests__/analyzer.test.ts +1 -0
- package/src/__tests__/provider.test.ts +1 -0
- package/src/__tests__/scoring.test.ts +1 -0
- package/src/analyzer.ts +32 -17
- package/src/scoring.ts +24 -11
- package/src/types.ts +2 -0
- package/dist/chunk-5EFFNN6L.mjs +0 -145
- package/dist/chunk-BBGJNBVI.mjs +0 -189
- package/dist/chunk-CGSYYULO.mjs +0 -145
- package/dist/chunk-E3YCVHHH.mjs +0 -152
- package/dist/chunk-FMK4O4O7.mjs +0 -143
- package/dist/chunk-NWJ6PSNT.mjs +0 -129
- package/dist/chunk-TSLAGWBV.mjs +0 -165
- package/dist/chunk-VLBPAYS3.mjs +0 -209
package/dist/chunk-FMK4O4O7.mjs
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
// src/analyzer.ts
|
|
9
|
-
import {
|
|
10
|
-
scanFiles,
|
|
11
|
-
calculateDocDrift,
|
|
12
|
-
getFileCommitTimestamps,
|
|
13
|
-
getLineRangeLastModifiedCached
|
|
14
|
-
} from "@aiready/core";
|
|
15
|
-
import { readFileSync } from "fs";
|
|
16
|
-
import { parse } from "@typescript-eslint/typescript-estree";
|
|
17
|
-
async function analyzeDocDrift(options) {
|
|
18
|
-
const files = await scanFiles(options);
|
|
19
|
-
const issues = [];
|
|
20
|
-
const staleMonths = options.staleMonths ?? 6;
|
|
21
|
-
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
22
|
-
let uncommentedExports = 0;
|
|
23
|
-
let totalExports = 0;
|
|
24
|
-
let outdatedComments = 0;
|
|
25
|
-
let undocumentedComplexity = 0;
|
|
26
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
27
|
-
let processed = 0;
|
|
28
|
-
for (const file of files) {
|
|
29
|
-
processed++;
|
|
30
|
-
options.onProgress?.(processed, files.length, `doc-drift: analyzing files`);
|
|
31
|
-
let code;
|
|
32
|
-
try {
|
|
33
|
-
code = readFileSync(file, "utf-8");
|
|
34
|
-
} catch {
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
let ast;
|
|
38
|
-
try {
|
|
39
|
-
ast = parse(code, {
|
|
40
|
-
jsx: file.endsWith(".tsx") || file.endsWith(".jsx"),
|
|
41
|
-
loc: true,
|
|
42
|
-
comment: true
|
|
43
|
-
});
|
|
44
|
-
} catch {
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
const comments = ast.comments || [];
|
|
48
|
-
let fileLineStamps;
|
|
49
|
-
for (const node of ast.body) {
|
|
50
|
-
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
51
|
-
const decl = node.declaration;
|
|
52
|
-
if (!decl) continue;
|
|
53
|
-
if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "VariableDeclaration") {
|
|
54
|
-
totalExports++;
|
|
55
|
-
const nodeLine = node.loc.start.line;
|
|
56
|
-
const jsdocs = comments.filter(
|
|
57
|
-
(c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1
|
|
58
|
-
);
|
|
59
|
-
if (jsdocs.length === 0) {
|
|
60
|
-
uncommentedExports++;
|
|
61
|
-
if (decl.type === "FunctionDeclaration" && decl.body?.loc) {
|
|
62
|
-
const lines = decl.body.loc.end.line - decl.body.loc.start.line;
|
|
63
|
-
if (lines > 20) undocumentedComplexity++;
|
|
64
|
-
}
|
|
65
|
-
} else {
|
|
66
|
-
const jsdoc = jsdocs[0];
|
|
67
|
-
const jsdocText = jsdoc.value;
|
|
68
|
-
if (decl.type === "FunctionDeclaration") {
|
|
69
|
-
const params = decl.params.map((p) => p.name || p.left && p.left.name).filter(Boolean);
|
|
70
|
-
const paramTags = Array.from(
|
|
71
|
-
jsdocText.matchAll(/@param\s+(?:\{[^}]+\}\s+)?([a-zA-Z0-9_]+)/g)
|
|
72
|
-
).map((m) => m[1]);
|
|
73
|
-
const missingParams = params.filter(
|
|
74
|
-
(p) => !paramTags.includes(p)
|
|
75
|
-
);
|
|
76
|
-
if (missingParams.length > 0) {
|
|
77
|
-
outdatedComments++;
|
|
78
|
-
issues.push({
|
|
79
|
-
type: "doc-drift",
|
|
80
|
-
severity: "major",
|
|
81
|
-
message: `JSDoc @param mismatch: function has parameters (${missingParams.join(", ")}) not documented in JSDoc.`,
|
|
82
|
-
location: { file, line: nodeLine }
|
|
83
|
-
});
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (!fileLineStamps) {
|
|
88
|
-
fileLineStamps = getFileCommitTimestamps(file);
|
|
89
|
-
}
|
|
90
|
-
const commentModified = getLineRangeLastModifiedCached(
|
|
91
|
-
fileLineStamps,
|
|
92
|
-
jsdoc.loc.start.line,
|
|
93
|
-
jsdoc.loc.end.line
|
|
94
|
-
);
|
|
95
|
-
const bodyModified = getLineRangeLastModifiedCached(
|
|
96
|
-
fileLineStamps,
|
|
97
|
-
decl.loc.start.line,
|
|
98
|
-
decl.loc.end.line
|
|
99
|
-
);
|
|
100
|
-
if (commentModified > 0 && bodyModified > 0) {
|
|
101
|
-
if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
|
|
102
|
-
outdatedComments++;
|
|
103
|
-
issues.push({
|
|
104
|
-
type: "doc-drift",
|
|
105
|
-
severity: "minor",
|
|
106
|
-
message: `JSDoc is significantly older than the function body implementation. Code may have drifted.`,
|
|
107
|
-
location: { file, line: jsdoc.loc.start.line }
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
const riskResult = calculateDocDrift({
|
|
117
|
-
uncommentedExports,
|
|
118
|
-
totalExports,
|
|
119
|
-
outdatedComments,
|
|
120
|
-
undocumentedComplexity
|
|
121
|
-
});
|
|
122
|
-
return {
|
|
123
|
-
summary: {
|
|
124
|
-
filesAnalyzed: files.length,
|
|
125
|
-
functionsAnalyzed: totalExports,
|
|
126
|
-
score: riskResult.score,
|
|
127
|
-
rating: riskResult.rating
|
|
128
|
-
},
|
|
129
|
-
issues,
|
|
130
|
-
rawData: {
|
|
131
|
-
uncommentedExports,
|
|
132
|
-
totalExports,
|
|
133
|
-
outdatedComments,
|
|
134
|
-
undocumentedComplexity
|
|
135
|
-
},
|
|
136
|
-
recommendations: riskResult.recommendations
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export {
|
|
141
|
-
__require,
|
|
142
|
-
analyzeDocDrift
|
|
143
|
-
};
|
package/dist/chunk-NWJ6PSNT.mjs
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
// src/analyzer.ts
|
|
9
|
-
import {
|
|
10
|
-
scanFiles,
|
|
11
|
-
calculateDocDrift,
|
|
12
|
-
getFileCommitTimestamps,
|
|
13
|
-
getLineRangeLastModifiedCached,
|
|
14
|
-
Severity,
|
|
15
|
-
IssueType,
|
|
16
|
-
emitProgress,
|
|
17
|
-
getParser
|
|
18
|
-
} from "@aiready/core";
|
|
19
|
-
import { readFileSync } from "fs";
|
|
20
|
-
async function analyzeDocDrift(options) {
|
|
21
|
-
const files = await scanFiles(options);
|
|
22
|
-
const issues = [];
|
|
23
|
-
const staleMonths = options.staleMonths ?? 6;
|
|
24
|
-
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
25
|
-
let uncommentedExports = 0;
|
|
26
|
-
let totalExports = 0;
|
|
27
|
-
let outdatedComments = 0;
|
|
28
|
-
let undocumentedComplexity = 0;
|
|
29
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
30
|
-
let processed = 0;
|
|
31
|
-
for (const file of files) {
|
|
32
|
-
processed++;
|
|
33
|
-
emitProgress(
|
|
34
|
-
processed,
|
|
35
|
-
files.length,
|
|
36
|
-
"doc-drift",
|
|
37
|
-
"analyzing files",
|
|
38
|
-
options.onProgress
|
|
39
|
-
);
|
|
40
|
-
const parser = getParser(file);
|
|
41
|
-
if (!parser) continue;
|
|
42
|
-
let code;
|
|
43
|
-
try {
|
|
44
|
-
code = readFileSync(file, "utf-8");
|
|
45
|
-
} catch {
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
try {
|
|
49
|
-
await parser.initialize();
|
|
50
|
-
const parseResult = parser.parse(code, file);
|
|
51
|
-
let fileLineStamps;
|
|
52
|
-
for (const exp of parseResult.exports) {
|
|
53
|
-
if (exp.type === "function" || exp.type === "class") {
|
|
54
|
-
totalExports++;
|
|
55
|
-
if (!exp.documentation) {
|
|
56
|
-
uncommentedExports++;
|
|
57
|
-
if (exp.loc) {
|
|
58
|
-
const lines = exp.loc.end.line - exp.loc.start.line;
|
|
59
|
-
if (lines > 20) undocumentedComplexity++;
|
|
60
|
-
}
|
|
61
|
-
} else {
|
|
62
|
-
const doc = exp.documentation;
|
|
63
|
-
const docContent = doc.content;
|
|
64
|
-
if (exp.type === "function" && exp.parameters) {
|
|
65
|
-
const params = exp.parameters;
|
|
66
|
-
const missingParams = params.filter(
|
|
67
|
-
(p) => !docContent.includes(p) && !docContent.toLowerCase().includes(p.toLowerCase())
|
|
68
|
-
);
|
|
69
|
-
if (missingParams.length > 0 && params.length > 2) {
|
|
70
|
-
outdatedComments++;
|
|
71
|
-
issues.push({
|
|
72
|
-
type: IssueType.DocDrift,
|
|
73
|
-
severity: Severity.Major,
|
|
74
|
-
message: `Documentation mismatch: function parameters (${missingParams.join(", ")}) are not mentioned in the docs.`,
|
|
75
|
-
location: { file, line: exp.loc?.start.line || 1 }
|
|
76
|
-
});
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (exp.loc) {
|
|
81
|
-
if (!fileLineStamps) {
|
|
82
|
-
fileLineStamps = getFileCommitTimestamps(file);
|
|
83
|
-
}
|
|
84
|
-
const bodyModified = getLineRangeLastModifiedCached(
|
|
85
|
-
fileLineStamps,
|
|
86
|
-
exp.loc.start.line,
|
|
87
|
-
exp.loc.end.line
|
|
88
|
-
);
|
|
89
|
-
if (bodyModified > 0) {
|
|
90
|
-
if (now - bodyModified < staleSeconds / 4 && exp.documentation.isStale === true) {
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
} catch (error) {
|
|
98
|
-
console.warn(`Doc-drift: Failed to parse ${file}: ${error}`);
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
const riskResult = calculateDocDrift({
|
|
103
|
-
uncommentedExports,
|
|
104
|
-
totalExports,
|
|
105
|
-
outdatedComments,
|
|
106
|
-
undocumentedComplexity
|
|
107
|
-
});
|
|
108
|
-
return {
|
|
109
|
-
summary: {
|
|
110
|
-
filesAnalyzed: files.length,
|
|
111
|
-
functionsAnalyzed: totalExports,
|
|
112
|
-
score: riskResult.score,
|
|
113
|
-
rating: riskResult.rating
|
|
114
|
-
},
|
|
115
|
-
issues,
|
|
116
|
-
rawData: {
|
|
117
|
-
uncommentedExports,
|
|
118
|
-
totalExports,
|
|
119
|
-
outdatedComments,
|
|
120
|
-
undocumentedComplexity
|
|
121
|
-
},
|
|
122
|
-
recommendations: riskResult.recommendations
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export {
|
|
127
|
-
__require,
|
|
128
|
-
analyzeDocDrift
|
|
129
|
-
};
|
package/dist/chunk-TSLAGWBV.mjs
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
// src/analyzer.ts
|
|
9
|
-
import { calculateDocDrift } from "@aiready/core";
|
|
10
|
-
import { readdirSync, statSync, readFileSync } from "fs";
|
|
11
|
-
import { join, extname } from "path";
|
|
12
|
-
import { parse } from "@typescript-eslint/typescript-estree";
|
|
13
|
-
import { execSync } from "child_process";
|
|
14
|
-
var SRC_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
15
|
-
var DEFAULT_EXCLUDES = ["node_modules", "dist", ".git", "coverage", ".turbo", "build"];
|
|
16
|
-
function collectFiles(dir, options, depth = 0) {
|
|
17
|
-
if (depth > 20) return [];
|
|
18
|
-
const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
19
|
-
let entries;
|
|
20
|
-
try {
|
|
21
|
-
entries = readdirSync(dir);
|
|
22
|
-
} catch {
|
|
23
|
-
return [];
|
|
24
|
-
}
|
|
25
|
-
const files = [];
|
|
26
|
-
for (const entry of entries) {
|
|
27
|
-
if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
|
|
28
|
-
const full = join(dir, entry);
|
|
29
|
-
let stat;
|
|
30
|
-
try {
|
|
31
|
-
stat = statSync(full);
|
|
32
|
-
} catch {
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
if (stat.isDirectory()) {
|
|
36
|
-
files.push(...collectFiles(full, options, depth + 1));
|
|
37
|
-
} else if (stat.isFile() && SRC_EXTENSIONS.has(extname(full))) {
|
|
38
|
-
if (!options.include || options.include.some((p) => full.includes(p))) {
|
|
39
|
-
files.push(full);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return files;
|
|
44
|
-
}
|
|
45
|
-
function getLineRangeLastModified(file, startLine, endLine) {
|
|
46
|
-
try {
|
|
47
|
-
const output = execSync(`git log -1 --format=%ct -L ${startLine},${endLine}:"${file}"`, {
|
|
48
|
-
encoding: "utf-8",
|
|
49
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
50
|
-
});
|
|
51
|
-
const match = output.trim().split("\n")[0];
|
|
52
|
-
if (match && !isNaN(parseInt(match, 10))) {
|
|
53
|
-
return parseInt(match, 10);
|
|
54
|
-
}
|
|
55
|
-
} catch {
|
|
56
|
-
}
|
|
57
|
-
return 0;
|
|
58
|
-
}
|
|
59
|
-
async function analyzeDocDrift(options) {
|
|
60
|
-
const rootDir = options.rootDir;
|
|
61
|
-
const files = collectFiles(rootDir, options);
|
|
62
|
-
const staleMonths = options.staleMonths ?? 6;
|
|
63
|
-
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
64
|
-
let uncommentedExports = 0;
|
|
65
|
-
let totalExports = 0;
|
|
66
|
-
let outdatedComments = 0;
|
|
67
|
-
let undocumentedComplexity = 0;
|
|
68
|
-
const issues = [];
|
|
69
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
70
|
-
for (const file of files) {
|
|
71
|
-
let code;
|
|
72
|
-
try {
|
|
73
|
-
code = readFileSync(file, "utf-8");
|
|
74
|
-
} catch {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
let ast;
|
|
78
|
-
try {
|
|
79
|
-
ast = parse(code, {
|
|
80
|
-
jsx: file.endsWith(".tsx") || file.endsWith(".jsx"),
|
|
81
|
-
loc: true,
|
|
82
|
-
comment: true
|
|
83
|
-
});
|
|
84
|
-
} catch {
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
const comments = ast.comments || [];
|
|
88
|
-
for (const node of ast.body) {
|
|
89
|
-
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
90
|
-
const decl = node.declaration;
|
|
91
|
-
if (!decl) continue;
|
|
92
|
-
if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "VariableDeclaration") {
|
|
93
|
-
totalExports++;
|
|
94
|
-
const nodeLine = node.loc.start.line;
|
|
95
|
-
const jsdocs = comments.filter((c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1);
|
|
96
|
-
if (jsdocs.length === 0) {
|
|
97
|
-
uncommentedExports++;
|
|
98
|
-
if (decl.type === "FunctionDeclaration" && decl.body?.loc) {
|
|
99
|
-
const lines = decl.body.loc.end.line - decl.body.loc.start.line;
|
|
100
|
-
if (lines > 20) undocumentedComplexity++;
|
|
101
|
-
}
|
|
102
|
-
} else {
|
|
103
|
-
const jsdoc = jsdocs[0];
|
|
104
|
-
const jsdocText = jsdoc.value;
|
|
105
|
-
if (decl.type === "FunctionDeclaration") {
|
|
106
|
-
const params = decl.params.map((p) => p.name || p.left && p.left.name).filter(Boolean);
|
|
107
|
-
const paramTags = Array.from(jsdocText.matchAll(/@param\s+(?:\{[^}]+\}\s+)?([a-zA-Z0-9_]+)/g)).map((m) => m[1]);
|
|
108
|
-
const missingParams = params.filter((p) => !paramTags.includes(p));
|
|
109
|
-
if (missingParams.length > 0) {
|
|
110
|
-
outdatedComments++;
|
|
111
|
-
issues.push({
|
|
112
|
-
type: "doc-drift",
|
|
113
|
-
severity: "major",
|
|
114
|
-
message: `JSDoc @param mismatch: function has parameters (${missingParams.join(", ")}) not documented in JSDoc.`,
|
|
115
|
-
location: { file, line: nodeLine }
|
|
116
|
-
});
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
const commentModified = getLineRangeLastModified(file, jsdoc.loc.start.line, jsdoc.loc.end.line);
|
|
121
|
-
const bodyModified = getLineRangeLastModified(file, decl.loc.start.line, decl.loc.end.line);
|
|
122
|
-
if (commentModified > 0 && bodyModified > 0) {
|
|
123
|
-
if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
|
|
124
|
-
outdatedComments++;
|
|
125
|
-
issues.push({
|
|
126
|
-
type: "doc-drift",
|
|
127
|
-
severity: "minor",
|
|
128
|
-
message: `JSDoc is significantly older than the function body implementation. Code may have drifted.`,
|
|
129
|
-
location: { file, line: jsdoc.loc.start.line }
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
const riskResult = calculateDocDrift({
|
|
139
|
-
uncommentedExports,
|
|
140
|
-
totalExports,
|
|
141
|
-
outdatedComments,
|
|
142
|
-
undocumentedComplexity
|
|
143
|
-
});
|
|
144
|
-
return {
|
|
145
|
-
summary: {
|
|
146
|
-
filesAnalyzed: files.length,
|
|
147
|
-
functionsAnalyzed: totalExports,
|
|
148
|
-
score: riskResult.score,
|
|
149
|
-
rating: riskResult.rating
|
|
150
|
-
},
|
|
151
|
-
issues,
|
|
152
|
-
rawData: {
|
|
153
|
-
uncommentedExports,
|
|
154
|
-
totalExports,
|
|
155
|
-
outdatedComments,
|
|
156
|
-
undocumentedComplexity
|
|
157
|
-
},
|
|
158
|
-
recommendations: riskResult.recommendations
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export {
|
|
163
|
-
__require,
|
|
164
|
-
analyzeDocDrift
|
|
165
|
-
};
|
package/dist/chunk-VLBPAYS3.mjs
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
// src/analyzer.ts
|
|
9
|
-
import { calculateDocDrift } from "@aiready/core";
|
|
10
|
-
import { readdirSync, statSync, readFileSync } from "fs";
|
|
11
|
-
import { join, extname } from "path";
|
|
12
|
-
import { parse } from "@typescript-eslint/typescript-estree";
|
|
13
|
-
import { execSync } from "child_process";
|
|
14
|
-
var SRC_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
15
|
-
var DEFAULT_EXCLUDES = [
|
|
16
|
-
"node_modules",
|
|
17
|
-
"dist",
|
|
18
|
-
".git",
|
|
19
|
-
"coverage",
|
|
20
|
-
".turbo",
|
|
21
|
-
"build"
|
|
22
|
-
];
|
|
23
|
-
function collectFiles(dir, options, depth = 0) {
|
|
24
|
-
if (depth > 20) return [];
|
|
25
|
-
const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
26
|
-
let entries;
|
|
27
|
-
try {
|
|
28
|
-
entries = readdirSync(dir);
|
|
29
|
-
} catch {
|
|
30
|
-
return [];
|
|
31
|
-
}
|
|
32
|
-
const files = [];
|
|
33
|
-
for (const entry of entries) {
|
|
34
|
-
if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
|
|
35
|
-
const full = join(dir, entry);
|
|
36
|
-
let stat;
|
|
37
|
-
try {
|
|
38
|
-
stat = statSync(full);
|
|
39
|
-
} catch {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
if (stat.isDirectory()) {
|
|
43
|
-
files.push(...collectFiles(full, options, depth + 1));
|
|
44
|
-
} else if (stat.isFile() && SRC_EXTENSIONS.has(extname(full))) {
|
|
45
|
-
if (!options.include || options.include.some((p) => full.includes(p))) {
|
|
46
|
-
files.push(full);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return files;
|
|
51
|
-
}
|
|
52
|
-
function getFileCommitTimestamps(file) {
|
|
53
|
-
const lineStamps = {};
|
|
54
|
-
try {
|
|
55
|
-
const output = execSync(`git blame -t "${file}"`, {
|
|
56
|
-
encoding: "utf-8",
|
|
57
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
58
|
-
});
|
|
59
|
-
const lines = output.split("\n");
|
|
60
|
-
for (const line of lines) {
|
|
61
|
-
if (!line) continue;
|
|
62
|
-
const match = line.match(/^\S+\s+\(.*?(\d{10,})\s+[-+]\d+\s+(\d+)\)/);
|
|
63
|
-
if (match) {
|
|
64
|
-
const ts = parseInt(match[1], 10);
|
|
65
|
-
const ln = parseInt(match[2], 10);
|
|
66
|
-
lineStamps[ln] = ts;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
} catch {
|
|
70
|
-
}
|
|
71
|
-
return lineStamps;
|
|
72
|
-
}
|
|
73
|
-
function getLineRangeLastModifiedCached(lineStamps, startLine, endLine) {
|
|
74
|
-
let latest = 0;
|
|
75
|
-
for (let i = startLine; i <= endLine; i++) {
|
|
76
|
-
if (lineStamps[i] && lineStamps[i] > latest) {
|
|
77
|
-
latest = lineStamps[i];
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return latest;
|
|
81
|
-
}
|
|
82
|
-
async function analyzeDocDrift(options) {
|
|
83
|
-
const rootDir = options.rootDir;
|
|
84
|
-
const files = collectFiles(rootDir, options);
|
|
85
|
-
const staleMonths = options.staleMonths ?? 6;
|
|
86
|
-
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
87
|
-
let uncommentedExports = 0;
|
|
88
|
-
let totalExports = 0;
|
|
89
|
-
let outdatedComments = 0;
|
|
90
|
-
let undocumentedComplexity = 0;
|
|
91
|
-
const issues = [];
|
|
92
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
93
|
-
let processed = 0;
|
|
94
|
-
for (const file of files) {
|
|
95
|
-
processed++;
|
|
96
|
-
options.onProgress?.(processed, files.length, `doc-drift: analyzing ${file.substring(rootDir.length + 1)}`);
|
|
97
|
-
let code;
|
|
98
|
-
try {
|
|
99
|
-
code = readFileSync(file, "utf-8");
|
|
100
|
-
} catch {
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
let ast;
|
|
104
|
-
try {
|
|
105
|
-
ast = parse(code, {
|
|
106
|
-
jsx: file.endsWith(".tsx") || file.endsWith(".jsx"),
|
|
107
|
-
loc: true,
|
|
108
|
-
comment: true
|
|
109
|
-
});
|
|
110
|
-
} catch {
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
const comments = ast.comments || [];
|
|
114
|
-
let fileLineStamps;
|
|
115
|
-
for (const node of ast.body) {
|
|
116
|
-
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
117
|
-
const decl = node.declaration;
|
|
118
|
-
if (!decl) continue;
|
|
119
|
-
if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "VariableDeclaration") {
|
|
120
|
-
totalExports++;
|
|
121
|
-
const nodeLine = node.loc.start.line;
|
|
122
|
-
const jsdocs = comments.filter(
|
|
123
|
-
(c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1
|
|
124
|
-
);
|
|
125
|
-
if (jsdocs.length === 0) {
|
|
126
|
-
uncommentedExports++;
|
|
127
|
-
if (decl.type === "FunctionDeclaration" && decl.body?.loc) {
|
|
128
|
-
const lines = decl.body.loc.end.line - decl.body.loc.start.line;
|
|
129
|
-
if (lines > 20) undocumentedComplexity++;
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
const jsdoc = jsdocs[0];
|
|
133
|
-
const jsdocText = jsdoc.value;
|
|
134
|
-
if (decl.type === "FunctionDeclaration") {
|
|
135
|
-
const params = decl.params.map((p) => p.name || p.left && p.left.name).filter(Boolean);
|
|
136
|
-
const paramTags = Array.from(
|
|
137
|
-
jsdocText.matchAll(/@param\s+(?:\{[^}]+\}\s+)?([a-zA-Z0-9_]+)/g)
|
|
138
|
-
).map((m) => m[1]);
|
|
139
|
-
const missingParams = params.filter(
|
|
140
|
-
(p) => !paramTags.includes(p)
|
|
141
|
-
);
|
|
142
|
-
if (missingParams.length > 0) {
|
|
143
|
-
outdatedComments++;
|
|
144
|
-
issues.push({
|
|
145
|
-
type: "doc-drift",
|
|
146
|
-
severity: "major",
|
|
147
|
-
message: `JSDoc @param mismatch: function has parameters (${missingParams.join(", ")}) not documented in JSDoc.`,
|
|
148
|
-
location: { file, line: nodeLine }
|
|
149
|
-
});
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (!fileLineStamps) {
|
|
154
|
-
fileLineStamps = getFileCommitTimestamps(file);
|
|
155
|
-
}
|
|
156
|
-
const commentModified = getLineRangeLastModifiedCached(
|
|
157
|
-
fileLineStamps,
|
|
158
|
-
jsdoc.loc.start.line,
|
|
159
|
-
jsdoc.loc.end.line
|
|
160
|
-
);
|
|
161
|
-
const bodyModified = getLineRangeLastModifiedCached(
|
|
162
|
-
fileLineStamps,
|
|
163
|
-
decl.loc.start.line,
|
|
164
|
-
decl.loc.end.line
|
|
165
|
-
);
|
|
166
|
-
if (commentModified > 0 && bodyModified > 0) {
|
|
167
|
-
if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
|
|
168
|
-
outdatedComments++;
|
|
169
|
-
issues.push({
|
|
170
|
-
type: "doc-drift",
|
|
171
|
-
severity: "minor",
|
|
172
|
-
message: `JSDoc is significantly older than the function body implementation. Code may have drifted.`,
|
|
173
|
-
location: { file, line: jsdoc.loc.start.line }
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
const riskResult = calculateDocDrift({
|
|
183
|
-
uncommentedExports,
|
|
184
|
-
totalExports,
|
|
185
|
-
outdatedComments,
|
|
186
|
-
undocumentedComplexity
|
|
187
|
-
});
|
|
188
|
-
return {
|
|
189
|
-
summary: {
|
|
190
|
-
filesAnalyzed: files.length,
|
|
191
|
-
functionsAnalyzed: totalExports,
|
|
192
|
-
score: riskResult.score,
|
|
193
|
-
rating: riskResult.rating
|
|
194
|
-
},
|
|
195
|
-
issues,
|
|
196
|
-
rawData: {
|
|
197
|
-
uncommentedExports,
|
|
198
|
-
totalExports,
|
|
199
|
-
outdatedComments,
|
|
200
|
-
undocumentedComplexity
|
|
201
|
-
},
|
|
202
|
-
recommendations: riskResult.recommendations
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export {
|
|
207
|
-
__require,
|
|
208
|
-
analyzeDocDrift
|
|
209
|
-
};
|