@aiready/doc-drift 0.1.6 → 0.1.10
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 +9 -9
- package/.turbo/turbo-lint.log +5 -0
- package/.turbo/turbo-test.log +5 -4
- package/dist/chunk-5EFFNN6L.mjs +145 -0
- package/dist/chunk-FMK4O4O7.mjs +143 -0
- package/dist/chunk-VLBPAYS3.mjs +209 -0
- package/dist/cli.js +13 -64
- package/dist/cli.mjs +1 -1
- package/dist/index.js +13 -64
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/analyzer.ts +22 -80
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/doc-drift@0.1.
|
|
3
|
+
> @aiready/doc-drift@0.1.10 build /Users/pengcao/projects/aiready/packages/doc-drift
|
|
4
4
|
> tsup src/index.ts src/cli.ts --format cjs,esm --dts
|
|
5
5
|
|
|
6
6
|
[34mCLI[39m Building entry: src/cli.ts, src/index.ts
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[34mCJS[39m Build start
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
|
|
13
|
-
[90m[[
|
|
13
|
+
[90m[[90m1:59:55 PM[90m][39m [43m[30m WARN [39m[49m [33m▲ [43;33m[[43;30mWARNING[43;33m][0m [1mThe condition "types" here will never be used as it comes after both "import" and "require"[0m [package.json]
|
|
14
14
|
|
|
15
15
|
package.json:33:6:
|
|
16
16
|
[37m 33 │ [32m"types"[37m: "./dist/index.d.ts"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
[90m[[
|
|
34
|
+
[90m[[90m1:59:55 PM[90m][39m [43m[30m WARN [39m[49m [33m▲ [43;33m[[43;30mWARNING[43;33m][0m [1mThe condition "types" here will never be used as it comes after both "import" and "require"[0m [package.json]
|
|
35
35
|
|
|
36
36
|
package.json:33:6:
|
|
37
37
|
[37m 33 │ [32m"types"[37m: "./dist/index.d.ts"
|
|
@@ -52,14 +52,14 @@
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
[32mESM[39m [1mdist/cli.mjs [22m[32m1.39 KB[39m
|
|
55
|
-
[32mESM[39m [1mdist/chunk-
|
|
55
|
+
[32mESM[39m [1mdist/chunk-FMK4O4O7.mjs [22m[32m4.79 KB[39m
|
|
56
56
|
[32mESM[39m [1mdist/index.mjs [22m[32m88.00 B[39m
|
|
57
|
-
[32mESM[39m ⚡️ Build success in
|
|
58
|
-
[32mCJS[39m [1mdist/
|
|
59
|
-
[32mCJS[39m [1mdist/
|
|
60
|
-
[32mCJS[39m ⚡️ Build success in
|
|
57
|
+
[32mESM[39m ⚡️ Build success in 163ms
|
|
58
|
+
[32mCJS[39m [1mdist/index.js [22m[32m5.50 KB[39m
|
|
59
|
+
[32mCJS[39m [1mdist/cli.js [22m[32m7.52 KB[39m
|
|
60
|
+
[32mCJS[39m ⚡️ Build success in 163ms
|
|
61
61
|
DTS Build start
|
|
62
|
-
DTS ⚡️ Build success in
|
|
62
|
+
DTS ⚡️ Build success in 4161ms
|
|
63
63
|
DTS dist/cli.d.ts 108.00 B
|
|
64
64
|
DTS dist/index.d.ts 950.00 B
|
|
65
65
|
DTS dist/cli.d.mts 108.00 B
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/doc-drift@0.1.
|
|
3
|
+
> @aiready/doc-drift@0.1.8 test /Users/pengcao/projects/aiready/packages/doc-drift
|
|
4
4
|
> vitest run
|
|
5
5
|
|
|
6
6
|
[?25l
|
|
7
7
|
[1m[46m RUN [49m[22m [36mv4.0.18 [39m[90m/Users/pengcao/projects/aiready/packages/doc-drift[39m
|
|
8
8
|
|
|
9
|
-
[32m✓[39m src/__tests__/analyzer.test.ts [2m([22m[2m1 test[22m[2m)[22m[
|
|
9
|
+
[32m✓[39m src/__tests__/analyzer.test.ts [2m([22m[2m1 test[22m[2m)[22m[33m 364[2mms[22m[39m
|
|
10
|
+
[33m[2m✓[22m[39m detects missing param documentation and uncommented complexity [33m 361[2mms[22m[39m
|
|
10
11
|
|
|
11
12
|
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
12
13
|
[2m Tests [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
13
|
-
[2m Start at [22m
|
|
14
|
-
[2m Duration [22m
|
|
14
|
+
[2m Start at [22m 00:56:24
|
|
15
|
+
[2m Duration [22m 5.43s[2m (transform 957ms, setup 0ms, import 3.81s, tests 364ms, environment 0ms)[22m
|
|
15
16
|
|
|
16
17
|
[?25h
|
|
@@ -0,0 +1,145 @@
|
|
|
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 rootDir = options.rootDir;
|
|
19
|
+
const files = await scanFiles(options);
|
|
20
|
+
const issues = [];
|
|
21
|
+
const results = [];
|
|
22
|
+
const staleMonths = options.staleMonths ?? 6;
|
|
23
|
+
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
24
|
+
let uncommentedExports = 0;
|
|
25
|
+
let totalExports = 0;
|
|
26
|
+
let outdatedComments = 0;
|
|
27
|
+
let undocumentedComplexity = 0;
|
|
28
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
29
|
+
let processed = 0;
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
processed++;
|
|
32
|
+
options.onProgress?.(processed, files.length, `doc-drift: analyzing files`);
|
|
33
|
+
let code;
|
|
34
|
+
try {
|
|
35
|
+
code = readFileSync(file, "utf-8");
|
|
36
|
+
} catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
let ast;
|
|
40
|
+
try {
|
|
41
|
+
ast = parse(code, {
|
|
42
|
+
jsx: file.endsWith(".tsx") || file.endsWith(".jsx"),
|
|
43
|
+
loc: true,
|
|
44
|
+
comment: true
|
|
45
|
+
});
|
|
46
|
+
} catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const comments = ast.comments || [];
|
|
50
|
+
let fileLineStamps;
|
|
51
|
+
for (const node of ast.body) {
|
|
52
|
+
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
53
|
+
const decl = node.declaration;
|
|
54
|
+
if (!decl) continue;
|
|
55
|
+
if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "VariableDeclaration") {
|
|
56
|
+
totalExports++;
|
|
57
|
+
const nodeLine = node.loc.start.line;
|
|
58
|
+
const jsdocs = comments.filter(
|
|
59
|
+
(c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1
|
|
60
|
+
);
|
|
61
|
+
if (jsdocs.length === 0) {
|
|
62
|
+
uncommentedExports++;
|
|
63
|
+
if (decl.type === "FunctionDeclaration" && decl.body?.loc) {
|
|
64
|
+
const lines = decl.body.loc.end.line - decl.body.loc.start.line;
|
|
65
|
+
if (lines > 20) undocumentedComplexity++;
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
const jsdoc = jsdocs[0];
|
|
69
|
+
const jsdocText = jsdoc.value;
|
|
70
|
+
if (decl.type === "FunctionDeclaration") {
|
|
71
|
+
const params = decl.params.map((p) => p.name || p.left && p.left.name).filter(Boolean);
|
|
72
|
+
const paramTags = Array.from(
|
|
73
|
+
jsdocText.matchAll(/@param\s+(?:\{[^}]+\}\s+)?([a-zA-Z0-9_]+)/g)
|
|
74
|
+
).map((m) => m[1]);
|
|
75
|
+
const missingParams = params.filter(
|
|
76
|
+
(p) => !paramTags.includes(p)
|
|
77
|
+
);
|
|
78
|
+
if (missingParams.length > 0) {
|
|
79
|
+
outdatedComments++;
|
|
80
|
+
issues.push({
|
|
81
|
+
type: "doc-drift",
|
|
82
|
+
severity: "major",
|
|
83
|
+
message: `JSDoc @param mismatch: function has parameters (${missingParams.join(", ")}) not documented in JSDoc.`,
|
|
84
|
+
location: { file, line: nodeLine }
|
|
85
|
+
});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!fileLineStamps) {
|
|
90
|
+
fileLineStamps = getFileCommitTimestamps(file);
|
|
91
|
+
}
|
|
92
|
+
const commentModified = getLineRangeLastModifiedCached(
|
|
93
|
+
fileLineStamps,
|
|
94
|
+
jsdoc.loc.start.line,
|
|
95
|
+
jsdoc.loc.end.line
|
|
96
|
+
);
|
|
97
|
+
const bodyModified = getLineRangeLastModifiedCached(
|
|
98
|
+
fileLineStamps,
|
|
99
|
+
decl.loc.start.line,
|
|
100
|
+
decl.loc.end.line
|
|
101
|
+
);
|
|
102
|
+
if (commentModified > 0 && bodyModified > 0) {
|
|
103
|
+
if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
|
|
104
|
+
outdatedComments++;
|
|
105
|
+
issues.push({
|
|
106
|
+
type: "doc-drift",
|
|
107
|
+
severity: "minor",
|
|
108
|
+
message: `JSDoc is significantly older than the function body implementation. Code may have drifted.`,
|
|
109
|
+
location: { file, line: jsdoc.loc.start.line }
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const riskResult = calculateDocDrift({
|
|
119
|
+
uncommentedExports,
|
|
120
|
+
totalExports,
|
|
121
|
+
outdatedComments,
|
|
122
|
+
undocumentedComplexity
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
summary: {
|
|
126
|
+
filesAnalyzed: files.length,
|
|
127
|
+
functionsAnalyzed: totalExports,
|
|
128
|
+
score: riskResult.score,
|
|
129
|
+
rating: riskResult.rating
|
|
130
|
+
},
|
|
131
|
+
issues,
|
|
132
|
+
rawData: {
|
|
133
|
+
uncommentedExports,
|
|
134
|
+
totalExports,
|
|
135
|
+
outdatedComments,
|
|
136
|
+
undocumentedComplexity
|
|
137
|
+
},
|
|
138
|
+
recommendations: riskResult.recommendations
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export {
|
|
143
|
+
__require,
|
|
144
|
+
analyzeDocDrift
|
|
145
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -38,76 +38,21 @@ var import_commander = require("commander");
|
|
|
38
38
|
// src/analyzer.ts
|
|
39
39
|
var import_core = require("@aiready/core");
|
|
40
40
|
var import_fs = require("fs");
|
|
41
|
-
var import_path = require("path");
|
|
42
41
|
var import_typescript_estree = require("@typescript-eslint/typescript-estree");
|
|
43
|
-
var import_child_process = require("child_process");
|
|
44
|
-
var SRC_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
45
|
-
var DEFAULT_EXCLUDES = [
|
|
46
|
-
"node_modules",
|
|
47
|
-
"dist",
|
|
48
|
-
".git",
|
|
49
|
-
"coverage",
|
|
50
|
-
".turbo",
|
|
51
|
-
"build"
|
|
52
|
-
];
|
|
53
|
-
function collectFiles(dir, options, depth = 0) {
|
|
54
|
-
if (depth > 20) return [];
|
|
55
|
-
const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
56
|
-
let entries;
|
|
57
|
-
try {
|
|
58
|
-
entries = (0, import_fs.readdirSync)(dir);
|
|
59
|
-
} catch {
|
|
60
|
-
return [];
|
|
61
|
-
}
|
|
62
|
-
const files = [];
|
|
63
|
-
for (const entry of entries) {
|
|
64
|
-
if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
|
|
65
|
-
const full = (0, import_path.join)(dir, entry);
|
|
66
|
-
let stat;
|
|
67
|
-
try {
|
|
68
|
-
stat = (0, import_fs.statSync)(full);
|
|
69
|
-
} catch {
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (stat.isDirectory()) {
|
|
73
|
-
files.push(...collectFiles(full, options, depth + 1));
|
|
74
|
-
} else if (stat.isFile() && SRC_EXTENSIONS.has((0, import_path.extname)(full))) {
|
|
75
|
-
if (!options.include || options.include.some((p) => full.includes(p))) {
|
|
76
|
-
files.push(full);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return files;
|
|
81
|
-
}
|
|
82
|
-
function getLineRangeLastModified(file, startLine, endLine) {
|
|
83
|
-
try {
|
|
84
|
-
const output = (0, import_child_process.execSync)(
|
|
85
|
-
`git log -1 --format=%ct -L ${startLine},${endLine}:"${file}"`,
|
|
86
|
-
{
|
|
87
|
-
encoding: "utf-8",
|
|
88
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
89
|
-
}
|
|
90
|
-
);
|
|
91
|
-
const match = output.trim().split("\n")[0];
|
|
92
|
-
if (match && !isNaN(parseInt(match, 10))) {
|
|
93
|
-
return parseInt(match, 10);
|
|
94
|
-
}
|
|
95
|
-
} catch {
|
|
96
|
-
}
|
|
97
|
-
return 0;
|
|
98
|
-
}
|
|
99
42
|
async function analyzeDocDrift(options) {
|
|
100
|
-
const
|
|
101
|
-
const
|
|
43
|
+
const files = await (0, import_core.scanFiles)(options);
|
|
44
|
+
const issues = [];
|
|
102
45
|
const staleMonths = options.staleMonths ?? 6;
|
|
103
46
|
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
104
47
|
let uncommentedExports = 0;
|
|
105
48
|
let totalExports = 0;
|
|
106
49
|
let outdatedComments = 0;
|
|
107
50
|
let undocumentedComplexity = 0;
|
|
108
|
-
const issues = [];
|
|
109
51
|
const now = Math.floor(Date.now() / 1e3);
|
|
52
|
+
let processed = 0;
|
|
110
53
|
for (const file of files) {
|
|
54
|
+
processed++;
|
|
55
|
+
options.onProgress?.(processed, files.length, `doc-drift: analyzing files`);
|
|
111
56
|
let code;
|
|
112
57
|
try {
|
|
113
58
|
code = (0, import_fs.readFileSync)(file, "utf-8");
|
|
@@ -125,6 +70,7 @@ async function analyzeDocDrift(options) {
|
|
|
125
70
|
continue;
|
|
126
71
|
}
|
|
127
72
|
const comments = ast.comments || [];
|
|
73
|
+
let fileLineStamps;
|
|
128
74
|
for (const node of ast.body) {
|
|
129
75
|
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
130
76
|
const decl = node.declaration;
|
|
@@ -163,13 +109,16 @@ async function analyzeDocDrift(options) {
|
|
|
163
109
|
continue;
|
|
164
110
|
}
|
|
165
111
|
}
|
|
166
|
-
|
|
167
|
-
file
|
|
112
|
+
if (!fileLineStamps) {
|
|
113
|
+
fileLineStamps = (0, import_core.getFileCommitTimestamps)(file);
|
|
114
|
+
}
|
|
115
|
+
const commentModified = (0, import_core.getLineRangeLastModifiedCached)(
|
|
116
|
+
fileLineStamps,
|
|
168
117
|
jsdoc.loc.start.line,
|
|
169
118
|
jsdoc.loc.end.line
|
|
170
119
|
);
|
|
171
|
-
const bodyModified =
|
|
172
|
-
|
|
120
|
+
const bodyModified = (0, import_core.getLineRangeLastModifiedCached)(
|
|
121
|
+
fileLineStamps,
|
|
173
122
|
decl.loc.start.line,
|
|
174
123
|
decl.loc.end.line
|
|
175
124
|
);
|
package/dist/cli.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -27,76 +27,21 @@ module.exports = __toCommonJS(index_exports);
|
|
|
27
27
|
// src/analyzer.ts
|
|
28
28
|
var import_core = require("@aiready/core");
|
|
29
29
|
var import_fs = require("fs");
|
|
30
|
-
var import_path = require("path");
|
|
31
30
|
var import_typescript_estree = require("@typescript-eslint/typescript-estree");
|
|
32
|
-
var import_child_process = require("child_process");
|
|
33
|
-
var SRC_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
34
|
-
var DEFAULT_EXCLUDES = [
|
|
35
|
-
"node_modules",
|
|
36
|
-
"dist",
|
|
37
|
-
".git",
|
|
38
|
-
"coverage",
|
|
39
|
-
".turbo",
|
|
40
|
-
"build"
|
|
41
|
-
];
|
|
42
|
-
function collectFiles(dir, options, depth = 0) {
|
|
43
|
-
if (depth > 20) return [];
|
|
44
|
-
const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
45
|
-
let entries;
|
|
46
|
-
try {
|
|
47
|
-
entries = (0, import_fs.readdirSync)(dir);
|
|
48
|
-
} catch {
|
|
49
|
-
return [];
|
|
50
|
-
}
|
|
51
|
-
const files = [];
|
|
52
|
-
for (const entry of entries) {
|
|
53
|
-
if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
|
|
54
|
-
const full = (0, import_path.join)(dir, entry);
|
|
55
|
-
let stat;
|
|
56
|
-
try {
|
|
57
|
-
stat = (0, import_fs.statSync)(full);
|
|
58
|
-
} catch {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (stat.isDirectory()) {
|
|
62
|
-
files.push(...collectFiles(full, options, depth + 1));
|
|
63
|
-
} else if (stat.isFile() && SRC_EXTENSIONS.has((0, import_path.extname)(full))) {
|
|
64
|
-
if (!options.include || options.include.some((p) => full.includes(p))) {
|
|
65
|
-
files.push(full);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return files;
|
|
70
|
-
}
|
|
71
|
-
function getLineRangeLastModified(file, startLine, endLine) {
|
|
72
|
-
try {
|
|
73
|
-
const output = (0, import_child_process.execSync)(
|
|
74
|
-
`git log -1 --format=%ct -L ${startLine},${endLine}:"${file}"`,
|
|
75
|
-
{
|
|
76
|
-
encoding: "utf-8",
|
|
77
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
78
|
-
}
|
|
79
|
-
);
|
|
80
|
-
const match = output.trim().split("\n")[0];
|
|
81
|
-
if (match && !isNaN(parseInt(match, 10))) {
|
|
82
|
-
return parseInt(match, 10);
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
}
|
|
86
|
-
return 0;
|
|
87
|
-
}
|
|
88
31
|
async function analyzeDocDrift(options) {
|
|
89
|
-
const
|
|
90
|
-
const
|
|
32
|
+
const files = await (0, import_core.scanFiles)(options);
|
|
33
|
+
const issues = [];
|
|
91
34
|
const staleMonths = options.staleMonths ?? 6;
|
|
92
35
|
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
93
36
|
let uncommentedExports = 0;
|
|
94
37
|
let totalExports = 0;
|
|
95
38
|
let outdatedComments = 0;
|
|
96
39
|
let undocumentedComplexity = 0;
|
|
97
|
-
const issues = [];
|
|
98
40
|
const now = Math.floor(Date.now() / 1e3);
|
|
41
|
+
let processed = 0;
|
|
99
42
|
for (const file of files) {
|
|
43
|
+
processed++;
|
|
44
|
+
options.onProgress?.(processed, files.length, `doc-drift: analyzing files`);
|
|
100
45
|
let code;
|
|
101
46
|
try {
|
|
102
47
|
code = (0, import_fs.readFileSync)(file, "utf-8");
|
|
@@ -114,6 +59,7 @@ async function analyzeDocDrift(options) {
|
|
|
114
59
|
continue;
|
|
115
60
|
}
|
|
116
61
|
const comments = ast.comments || [];
|
|
62
|
+
let fileLineStamps;
|
|
117
63
|
for (const node of ast.body) {
|
|
118
64
|
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
119
65
|
const decl = node.declaration;
|
|
@@ -152,13 +98,16 @@ async function analyzeDocDrift(options) {
|
|
|
152
98
|
continue;
|
|
153
99
|
}
|
|
154
100
|
}
|
|
155
|
-
|
|
156
|
-
file
|
|
101
|
+
if (!fileLineStamps) {
|
|
102
|
+
fileLineStamps = (0, import_core.getFileCommitTimestamps)(file);
|
|
103
|
+
}
|
|
104
|
+
const commentModified = (0, import_core.getLineRangeLastModifiedCached)(
|
|
105
|
+
fileLineStamps,
|
|
157
106
|
jsdoc.loc.start.line,
|
|
158
107
|
jsdoc.loc.end.line
|
|
159
108
|
);
|
|
160
|
-
const bodyModified =
|
|
161
|
-
|
|
109
|
+
const bodyModified = (0, import_core.getLineRangeLastModifiedCached)(
|
|
110
|
+
fileLineStamps,
|
|
162
111
|
decl.loc.start.line,
|
|
163
112
|
decl.loc.end.line
|
|
164
113
|
);
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/doc-drift",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "AI-Readiness: Documentation Drift Detection",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"commander": "^14.0.0",
|
|
11
11
|
"glob": "^13.0.0",
|
|
12
12
|
"picocolors": "^1.0.0",
|
|
13
|
-
"@aiready/core": "0.9.
|
|
13
|
+
"@aiready/core": "0.9.37"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@types/node": "^24.0.0",
|
package/src/analyzer.ts
CHANGED
|
@@ -1,85 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
scanFiles,
|
|
3
|
+
calculateDocDrift,
|
|
4
|
+
getFileCommitTimestamps,
|
|
5
|
+
getLineRangeLastModifiedCached,
|
|
6
|
+
} from '@aiready/core';
|
|
2
7
|
import type { DocDriftOptions, DocDriftReport, DocDriftIssue } from './types';
|
|
3
|
-
import {
|
|
4
|
-
import { join, extname } from 'path';
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
5
9
|
import { parse } from '@typescript-eslint/typescript-estree';
|
|
6
10
|
import type { TSESTree } from '@typescript-eslint/types';
|
|
7
|
-
import { execSync } from 'child_process';
|
|
8
|
-
|
|
9
|
-
const SRC_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
10
|
-
const DEFAULT_EXCLUDES = [
|
|
11
|
-
'node_modules',
|
|
12
|
-
'dist',
|
|
13
|
-
'.git',
|
|
14
|
-
'coverage',
|
|
15
|
-
'.turbo',
|
|
16
|
-
'build',
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
function collectFiles(
|
|
20
|
-
dir: string,
|
|
21
|
-
options: DocDriftOptions,
|
|
22
|
-
depth = 0
|
|
23
|
-
): string[] {
|
|
24
|
-
if (depth > 20) return [];
|
|
25
|
-
const excludes = [...DEFAULT_EXCLUDES, ...(options.exclude ?? [])];
|
|
26
|
-
let entries: string[];
|
|
27
|
-
try {
|
|
28
|
-
entries = readdirSync(dir);
|
|
29
|
-
} catch {
|
|
30
|
-
return [];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const files: string[] = [];
|
|
34
|
-
for (const entry of entries) {
|
|
35
|
-
if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
|
|
36
|
-
const full = join(dir, entry);
|
|
37
|
-
let stat;
|
|
38
|
-
try {
|
|
39
|
-
stat = statSync(full);
|
|
40
|
-
} catch {
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
if (stat.isDirectory()) {
|
|
44
|
-
files.push(...collectFiles(full, options, depth + 1));
|
|
45
|
-
} else if (stat.isFile() && SRC_EXTENSIONS.has(extname(full))) {
|
|
46
|
-
if (!options.include || options.include.some((p) => full.includes(p))) {
|
|
47
|
-
files.push(full);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return files;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function getLineRangeLastModified(
|
|
55
|
-
file: string,
|
|
56
|
-
startLine: number,
|
|
57
|
-
endLine: number
|
|
58
|
-
): number {
|
|
59
|
-
try {
|
|
60
|
-
// format %ct is committer date, UNIX timestamp
|
|
61
|
-
const output = execSync(
|
|
62
|
-
`git log -1 --format=%ct -L ${startLine},${endLine}:"${file}"`,
|
|
63
|
-
{
|
|
64
|
-
encoding: 'utf-8',
|
|
65
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
66
|
-
}
|
|
67
|
-
);
|
|
68
|
-
const match = output.trim().split('\n')[0];
|
|
69
|
-
if (match && !isNaN(parseInt(match, 10))) {
|
|
70
|
-
return parseInt(match, 10);
|
|
71
|
-
}
|
|
72
|
-
} catch {
|
|
73
|
-
// Ignore errors (file untracked, new file, etc)
|
|
74
|
-
}
|
|
75
|
-
return 0; // Unknown or not committed
|
|
76
|
-
}
|
|
77
11
|
|
|
78
12
|
export async function analyzeDocDrift(
|
|
79
13
|
options: DocDriftOptions
|
|
80
14
|
): Promise<DocDriftReport> {
|
|
81
|
-
|
|
82
|
-
const files =
|
|
15
|
+
// Use core scanFiles which respects .gitignore recursively
|
|
16
|
+
const files = await scanFiles(options);
|
|
17
|
+
const issues: DocDriftIssue[] = [];
|
|
83
18
|
const staleMonths = options.staleMonths ?? 6;
|
|
84
19
|
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
85
20
|
|
|
@@ -88,10 +23,13 @@ export async function analyzeDocDrift(
|
|
|
88
23
|
let outdatedComments = 0;
|
|
89
24
|
let undocumentedComplexity = 0;
|
|
90
25
|
|
|
91
|
-
const issues: DocDriftIssue[] = [];
|
|
92
26
|
const now = Math.floor(Date.now() / 1000);
|
|
93
27
|
|
|
28
|
+
let processed = 0;
|
|
94
29
|
for (const file of files) {
|
|
30
|
+
processed++;
|
|
31
|
+
options.onProgress?.(processed, files.length, `doc-drift: analyzing files`);
|
|
32
|
+
|
|
95
33
|
let code: string;
|
|
96
34
|
try {
|
|
97
35
|
code = readFileSync(file, 'utf-8');
|
|
@@ -111,6 +49,7 @@ export async function analyzeDocDrift(
|
|
|
111
49
|
}
|
|
112
50
|
|
|
113
51
|
const comments = ast.comments || [];
|
|
52
|
+
let fileLineStamps: Record<number, number> | undefined;
|
|
114
53
|
|
|
115
54
|
for (const node of ast.body) {
|
|
116
55
|
if (
|
|
@@ -174,13 +113,16 @@ export async function analyzeDocDrift(
|
|
|
174
113
|
}
|
|
175
114
|
|
|
176
115
|
// Timestamp comparison
|
|
177
|
-
|
|
178
|
-
file
|
|
116
|
+
if (!fileLineStamps) {
|
|
117
|
+
fileLineStamps = getFileCommitTimestamps(file);
|
|
118
|
+
}
|
|
119
|
+
const commentModified = getLineRangeLastModifiedCached(
|
|
120
|
+
fileLineStamps,
|
|
179
121
|
jsdoc.loc.start.line,
|
|
180
122
|
jsdoc.loc.end.line
|
|
181
123
|
);
|
|
182
|
-
const bodyModified =
|
|
183
|
-
|
|
124
|
+
const bodyModified = getLineRangeLastModifiedCached(
|
|
125
|
+
fileLineStamps,
|
|
184
126
|
decl.loc.start.line,
|
|
185
127
|
decl.loc.end.line
|
|
186
128
|
);
|