ts-unused 1.0.1 → 1.0.3
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/README.md +271 -1
- package/dist/analyzeFunctionReturnTypes.d.ts +6 -0
- package/dist/analyzeFunctionReturnTypes.js +235 -0
- package/dist/analyzeInterfaces.d.ts +7 -0
- package/dist/analyzeInterfaces.js +51 -0
- package/dist/analyzeProject.d.ts +13 -0
- package/dist/analyzeProject.js +131 -0
- package/dist/analyzeTypeAliases.d.ts +9 -0
- package/dist/analyzeTypeAliases.js +70 -0
- package/dist/checkExportUsage.d.ts +7 -0
- package/dist/checkExportUsage.js +126 -0
- package/dist/checkGitStatus.d.ts +7 -0
- package/dist/checkGitStatus.js +49 -0
- package/dist/cli.js +267 -63
- package/dist/config.d.ts +96 -0
- package/dist/config.js +50 -0
- package/dist/extractTodoComment.d.ts +6 -0
- package/dist/extractTodoComment.js +27 -0
- package/dist/findNeverReturnedTypes.d.ts +6 -0
- package/dist/findNeverReturnedTypes.js +36 -0
- package/dist/findStructurallyEquivalentProperties.d.ts +12 -0
- package/dist/findStructurallyEquivalentProperties.js +60 -0
- package/dist/findUnusedExports.d.ts +7 -0
- package/dist/findUnusedExports.js +36 -0
- package/dist/findUnusedProperties.d.ts +7 -0
- package/dist/findUnusedProperties.js +32 -0
- package/dist/fixProject.d.ts +13 -0
- package/dist/fixProject.js +549 -0
- package/dist/formatResults.d.ts +2 -0
- package/dist/formatResults.js +112 -0
- package/dist/hasNoCheck.d.ts +2 -0
- package/dist/hasNoCheck.js +8 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +9 -0
- package/dist/isPropertyUnused.d.ts +7 -0
- package/dist/isPropertyUnused.js +48 -0
- package/dist/isTestFile.d.ts +14 -0
- package/dist/isTestFile.js +23 -0
- package/dist/loadConfig.d.ts +20 -0
- package/dist/loadConfig.js +54 -0
- package/dist/patternMatcher.d.ts +26 -0
- package/dist/patternMatcher.js +83 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.js +1 -0
- package/package.json +12 -3
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { Project } from "ts-morph";
|
|
3
|
+
import { defaultConfig } from "./config";
|
|
4
|
+
import { findNeverReturnedTypes } from "./findNeverReturnedTypes";
|
|
5
|
+
import { findUnusedExports } from "./findUnusedExports";
|
|
6
|
+
import { findUnusedProperties } from "./findUnusedProperties";
|
|
7
|
+
import { hasNoCheck } from "./hasNoCheck";
|
|
8
|
+
import { createIsTestFile, isTestFile as defaultIsTestFile } from "./isTestFile";
|
|
9
|
+
import { matchesFilePattern } from "./patternMatcher";
|
|
10
|
+
export function analyzeProject(tsConfigPath, onProgress, targetFilePath, isTestFileOrOptions) {
|
|
11
|
+
// Handle backward compatibility: can pass IsTestFileFn directly or options object
|
|
12
|
+
let options = {};
|
|
13
|
+
if (typeof isTestFileOrOptions === "function") {
|
|
14
|
+
options = { isTestFile: isTestFileOrOptions };
|
|
15
|
+
}
|
|
16
|
+
else if (isTestFileOrOptions) {
|
|
17
|
+
options = isTestFileOrOptions;
|
|
18
|
+
}
|
|
19
|
+
const config = {
|
|
20
|
+
...defaultConfig,
|
|
21
|
+
...options.config,
|
|
22
|
+
};
|
|
23
|
+
// Determine the isTestFile function
|
|
24
|
+
const isTestFile = options.isTestFile
|
|
25
|
+
? options.isTestFile
|
|
26
|
+
: config.testFilePatterns
|
|
27
|
+
? createIsTestFile(config.testFilePatterns)
|
|
28
|
+
: defaultIsTestFile;
|
|
29
|
+
const project = new Project({
|
|
30
|
+
tsConfigFilePath: tsConfigPath,
|
|
31
|
+
});
|
|
32
|
+
const tsConfigDir = path.dirname(tsConfigPath);
|
|
33
|
+
// Get total file count for progress reporting
|
|
34
|
+
const allSourceFiles = project.getSourceFiles();
|
|
35
|
+
const filesToAnalyze = allSourceFiles.filter((sf) => {
|
|
36
|
+
if (isTestFile(sf)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (hasNoCheck(sf)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (targetFilePath && sf.getFilePath() !== targetFilePath) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
// Check if file should be ignored based on patterns
|
|
46
|
+
if (config.ignoreFilePatterns.length > 0 && matchesFilePattern(sf.getFilePath(), config.ignoreFilePatterns)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
51
|
+
const totalFiles = filesToAnalyze.length;
|
|
52
|
+
let currentFile = 0;
|
|
53
|
+
const filesProcessed = new Set();
|
|
54
|
+
const progressCallback = onProgress
|
|
55
|
+
? (filePath) => {
|
|
56
|
+
// Only increment once per file (called from both exports and properties analysis)
|
|
57
|
+
if (!filesProcessed.has(filePath)) {
|
|
58
|
+
filesProcessed.add(filePath);
|
|
59
|
+
currentFile++;
|
|
60
|
+
onProgress(currentFile, totalFiles, filePath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
: undefined;
|
|
64
|
+
// Build options for child functions
|
|
65
|
+
const exportOptions = {
|
|
66
|
+
ignoreFilePatterns: config.ignoreFilePatterns,
|
|
67
|
+
ignoreExports: config.ignoreExports,
|
|
68
|
+
ignoreModuleAugmentations: config.ignoreModuleAugmentations,
|
|
69
|
+
};
|
|
70
|
+
const propertyOptions = {
|
|
71
|
+
ignoreFilePatterns: config.ignoreFilePatterns,
|
|
72
|
+
ignoreProperties: config.ignoreProperties,
|
|
73
|
+
ignoreTypes: config.ignoreTypes,
|
|
74
|
+
};
|
|
75
|
+
const neverReturnedOptions = {
|
|
76
|
+
ignoreFilePatterns: config.ignoreFilePatterns,
|
|
77
|
+
};
|
|
78
|
+
// Analyze based on config flags
|
|
79
|
+
const unusedExports = config.analyzeExports
|
|
80
|
+
? findUnusedExports(project, tsConfigDir, isTestFile, progressCallback, targetFilePath, exportOptions)
|
|
81
|
+
: [];
|
|
82
|
+
const unusedProperties = config.analyzeProperties
|
|
83
|
+
? findUnusedProperties(project, tsConfigDir, isTestFile, progressCallback, targetFilePath, propertyOptions)
|
|
84
|
+
: [];
|
|
85
|
+
const neverReturnedTypes = config.analyzeNeverReturnedTypes
|
|
86
|
+
? findNeverReturnedTypes(project, tsConfigDir, isTestFile, progressCallback, targetFilePath, neverReturnedOptions)
|
|
87
|
+
: [];
|
|
88
|
+
// Identify completely unused files (where all exports are unused)
|
|
89
|
+
const unusedFiles = [];
|
|
90
|
+
if (config.detectUnusedFiles) {
|
|
91
|
+
const fileExportCounts = new Map();
|
|
92
|
+
// Count total exports per file
|
|
93
|
+
for (const sourceFile of filesToAnalyze) {
|
|
94
|
+
const filePath = path.relative(tsConfigDir, sourceFile.getFilePath());
|
|
95
|
+
const exports = sourceFile.getExportedDeclarations();
|
|
96
|
+
const totalExports = exports.size;
|
|
97
|
+
if (totalExports > 0) {
|
|
98
|
+
fileExportCounts.set(filePath, { total: totalExports, unused: 0, testOnly: 0 });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Count unused exports per file
|
|
102
|
+
for (const unusedExport of unusedExports) {
|
|
103
|
+
const counts = fileExportCounts.get(unusedExport.filePath);
|
|
104
|
+
if (counts) {
|
|
105
|
+
counts.unused++;
|
|
106
|
+
if (unusedExport.onlyUsedInTests) {
|
|
107
|
+
counts.testOnly++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Identify files where all exports are unused AND not just test-only
|
|
112
|
+
// Files with any test-only exports should show individual exports with [INFO], not be listed as completely unused
|
|
113
|
+
for (const [filePath, counts] of fileExportCounts.entries()) {
|
|
114
|
+
// Only add to unusedFiles if:
|
|
115
|
+
// 1. All exports are "unused" (in the unusedExports list)
|
|
116
|
+
// 2. NONE of the exports are test-only (all are completely unused with severity: error)
|
|
117
|
+
const allExportsUnused = counts.total > 0 && counts.unused === counts.total;
|
|
118
|
+
const hasAnyTestOnlyExports = counts.testOnly > 0;
|
|
119
|
+
if (allExportsUnused && !hasAnyTestOnlyExports) {
|
|
120
|
+
unusedFiles.push(filePath);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const results = {
|
|
125
|
+
unusedExports,
|
|
126
|
+
unusedProperties,
|
|
127
|
+
unusedFiles,
|
|
128
|
+
neverReturnedTypes,
|
|
129
|
+
};
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Project, type SourceFile, type TypeAliasDeclaration, type TypeElementTypes } from "ts-morph";
|
|
2
|
+
import type { IsTestFileFn, UnusedPropertyResult } from "./types";
|
|
3
|
+
export interface AnalyzeTypeAliasesOptions {
|
|
4
|
+
ignoreProperties?: string[];
|
|
5
|
+
ignoreTypes?: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function analyzeTypeLiteralMember(member: TypeElementTypes, typeName: string, sourceFile: SourceFile, tsConfigDir: string, isTestFile: IsTestFileFn, results: UnusedPropertyResult[], project: Project, options?: AnalyzeTypeAliasesOptions): void;
|
|
8
|
+
export declare function analyzeTypeAlias(typeAlias: TypeAliasDeclaration, sourceFile: SourceFile, tsConfigDir: string, isTestFile: IsTestFileFn, results: UnusedPropertyResult[], project: Project, options?: AnalyzeTypeAliasesOptions): void;
|
|
9
|
+
export declare function analyzeTypeAliases(sourceFile: SourceFile, tsConfigDir: string, isTestFile: IsTestFileFn, results: UnusedPropertyResult[], project: Project, options?: AnalyzeTypeAliasesOptions): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { Node } from "ts-morph";
|
|
3
|
+
import { extractTodoComment } from "./extractTodoComment";
|
|
4
|
+
import { isPropertyUnused } from "./isPropertyUnused";
|
|
5
|
+
import { matchesPattern } from "./patternMatcher";
|
|
6
|
+
export function analyzeTypeLiteralMember(member, typeName, sourceFile, tsConfigDir, isTestFile, results, project, options = {}) {
|
|
7
|
+
const { ignoreProperties = [] } = options;
|
|
8
|
+
if (!Node.isPropertySignature(member)) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const propertyName = member.getName();
|
|
12
|
+
// Skip ignored properties
|
|
13
|
+
if (ignoreProperties.length > 0 && matchesPattern(propertyName, ignoreProperties)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const usage = isPropertyUnused(member, isTestFile, project);
|
|
17
|
+
if (!usage.isUnusedOrTestOnly) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const relativePath = path.relative(tsConfigDir, sourceFile.getFilePath());
|
|
21
|
+
const todoComment = extractTodoComment(member);
|
|
22
|
+
// Determine severity: warning for TODO, info for test-only, error for completely unused
|
|
23
|
+
let severity = "error";
|
|
24
|
+
if (todoComment) {
|
|
25
|
+
severity = "warning";
|
|
26
|
+
}
|
|
27
|
+
else if (usage.onlyUsedInTests) {
|
|
28
|
+
severity = "info";
|
|
29
|
+
}
|
|
30
|
+
const startPos = member.getStart();
|
|
31
|
+
const lineStartPos = member.getStartLinePos();
|
|
32
|
+
const character = startPos - lineStartPos + 1;
|
|
33
|
+
const endCharacter = character + propertyName.length;
|
|
34
|
+
const result = {
|
|
35
|
+
filePath: relativePath,
|
|
36
|
+
typeName,
|
|
37
|
+
propertyName,
|
|
38
|
+
line: member.getStartLineNumber(),
|
|
39
|
+
character,
|
|
40
|
+
endCharacter,
|
|
41
|
+
todoComment,
|
|
42
|
+
severity,
|
|
43
|
+
onlyUsedInTests: usage.onlyUsedInTests,
|
|
44
|
+
};
|
|
45
|
+
results.push(result);
|
|
46
|
+
}
|
|
47
|
+
export function analyzeTypeAlias(typeAlias, sourceFile, tsConfigDir, isTestFile, results, project, options = {}) {
|
|
48
|
+
const { ignoreTypes = [] } = options;
|
|
49
|
+
const typeName = typeAlias.getName();
|
|
50
|
+
// Skip ignored types
|
|
51
|
+
if (ignoreTypes.length > 0 && matchesPattern(typeName, ignoreTypes)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const typeNode = typeAlias.getTypeNode();
|
|
55
|
+
if (!typeNode) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!Node.isTypeLiteral(typeNode)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const member of typeNode.getMembers()) {
|
|
62
|
+
analyzeTypeLiteralMember(member, typeName, sourceFile, tsConfigDir, isTestFile, results, project, options);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function analyzeTypeAliases(sourceFile, tsConfigDir, isTestFile, results, project, options = {}) {
|
|
66
|
+
const typeAliases = sourceFile.getTypeAliases();
|
|
67
|
+
for (const typeAlias of typeAliases) {
|
|
68
|
+
analyzeTypeAlias(typeAlias, sourceFile, tsConfigDir, isTestFile, results, project, options);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Node, type SourceFile } from "ts-morph";
|
|
2
|
+
import type { IsTestFileFn, UnusedExportResult } from "./types";
|
|
3
|
+
export interface CheckExportOptions {
|
|
4
|
+
ignoreExports?: string[];
|
|
5
|
+
ignoreModuleAugmentations?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function checkExportUsage(exportName: string, declarations: readonly Node[], sourceFile: SourceFile, tsConfigDir: string, isTestFile: IsTestFileFn, options?: CheckExportOptions): UnusedExportResult | null;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { Node } from "ts-morph";
|
|
3
|
+
import { matchesPattern } from "./patternMatcher";
|
|
4
|
+
function getExportKind(declaration) {
|
|
5
|
+
if (Node.isFunctionDeclaration(declaration)) {
|
|
6
|
+
return "function";
|
|
7
|
+
}
|
|
8
|
+
if (Node.isClassDeclaration(declaration)) {
|
|
9
|
+
return "class";
|
|
10
|
+
}
|
|
11
|
+
if (Node.isInterfaceDeclaration(declaration)) {
|
|
12
|
+
return "interface";
|
|
13
|
+
}
|
|
14
|
+
if (Node.isTypeAliasDeclaration(declaration)) {
|
|
15
|
+
return "type";
|
|
16
|
+
}
|
|
17
|
+
if (Node.isEnumDeclaration(declaration)) {
|
|
18
|
+
return "enum";
|
|
19
|
+
}
|
|
20
|
+
if (Node.isModuleDeclaration(declaration)) {
|
|
21
|
+
return "namespace";
|
|
22
|
+
}
|
|
23
|
+
if (Node.isVariableDeclaration(declaration)) {
|
|
24
|
+
const parent = declaration.getParent();
|
|
25
|
+
if (Node.isVariableDeclarationList(parent)) {
|
|
26
|
+
const declarationKind = parent.getDeclarationKind();
|
|
27
|
+
if (declarationKind === "const") {
|
|
28
|
+
return "const";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return "variable";
|
|
32
|
+
}
|
|
33
|
+
return "export";
|
|
34
|
+
}
|
|
35
|
+
function getNameNode(declaration) {
|
|
36
|
+
if (Node.isFunctionDeclaration(declaration) ||
|
|
37
|
+
Node.isClassDeclaration(declaration) ||
|
|
38
|
+
Node.isInterfaceDeclaration(declaration) ||
|
|
39
|
+
Node.isTypeAliasDeclaration(declaration) ||
|
|
40
|
+
Node.isEnumDeclaration(declaration) ||
|
|
41
|
+
Node.isVariableDeclaration(declaration)) {
|
|
42
|
+
return declaration.getNameNode();
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Checks if a declaration is a module augmentation (declare module "...").
|
|
48
|
+
*/
|
|
49
|
+
function isModuleAugmentation(declaration) {
|
|
50
|
+
if (Node.isModuleDeclaration(declaration)) {
|
|
51
|
+
// Module augmentations have a string literal as name (e.g., "library-name")
|
|
52
|
+
const nameNode = declaration.getNameNode();
|
|
53
|
+
if (Node.isStringLiteral(nameNode)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
export function checkExportUsage(exportName, declarations, sourceFile, tsConfigDir, isTestFile, options = {}) {
|
|
60
|
+
const { ignoreExports = [], ignoreModuleAugmentations = true } = options;
|
|
61
|
+
const firstDeclaration = declarations[0];
|
|
62
|
+
if (!firstDeclaration) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
// Check if export should be ignored
|
|
66
|
+
if (ignoreExports.length > 0 && matchesPattern(exportName, ignoreExports)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
// Check if this is a module augmentation that should be ignored
|
|
70
|
+
if (ignoreModuleAugmentations && isModuleAugmentation(firstDeclaration)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
// Only report symbols defined in this file, not re-exports
|
|
74
|
+
const declarationSourceFile = firstDeclaration.getSourceFile();
|
|
75
|
+
if (declarationSourceFile.getFilePath() !== sourceFile.getFilePath()) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (!Node.isReferenceFindable(firstDeclaration)) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const references = firstDeclaration.findReferences();
|
|
82
|
+
// Count references by type
|
|
83
|
+
let totalReferences = 0;
|
|
84
|
+
let testReferences = 0;
|
|
85
|
+
let nonTestReferences = 0;
|
|
86
|
+
for (const refGroup of references) {
|
|
87
|
+
const refs = refGroup.getReferences();
|
|
88
|
+
for (const ref of refs) {
|
|
89
|
+
const refSourceFile = ref.getSourceFile();
|
|
90
|
+
totalReferences++;
|
|
91
|
+
if (isTestFile(refSourceFile)) {
|
|
92
|
+
testReferences++;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
nonTestReferences++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// An export is unused if it only has 1 reference (the definition itself)
|
|
100
|
+
const onlyUsedInTests = nonTestReferences === 1 && testReferences > 0;
|
|
101
|
+
if (totalReferences > 1 && !onlyUsedInTests) {
|
|
102
|
+
return null; // Used in production code
|
|
103
|
+
}
|
|
104
|
+
const kind = getExportKind(firstDeclaration);
|
|
105
|
+
const relativePath = path.relative(tsConfigDir, sourceFile.getFilePath());
|
|
106
|
+
// Determine severity: info for test-only, error for completely unused
|
|
107
|
+
const severity = onlyUsedInTests ? "info" : "error";
|
|
108
|
+
// Get the name node for accurate position highlighting
|
|
109
|
+
const nameNode = getNameNode(firstDeclaration);
|
|
110
|
+
const positionNode = nameNode || firstDeclaration;
|
|
111
|
+
const startPos = positionNode.getStart();
|
|
112
|
+
const lineStartPos = positionNode.getStartLinePos();
|
|
113
|
+
const character = startPos - lineStartPos + 1;
|
|
114
|
+
const endCharacter = character + exportName.length;
|
|
115
|
+
const result = {
|
|
116
|
+
filePath: relativePath,
|
|
117
|
+
exportName,
|
|
118
|
+
line: firstDeclaration.getStartLineNumber(),
|
|
119
|
+
character,
|
|
120
|
+
endCharacter,
|
|
121
|
+
kind,
|
|
122
|
+
severity,
|
|
123
|
+
onlyUsedInTests,
|
|
124
|
+
};
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check which files have local git changes (modified, staged, or untracked)
|
|
3
|
+
* Note: Checks the entire git repository, not just the working directory
|
|
4
|
+
* @param workingDir - The directory to check git status in (used to find git root)
|
|
5
|
+
* @returns Set of absolute file paths that have local changes
|
|
6
|
+
*/
|
|
7
|
+
export declare function checkGitStatus(workingDir: string): Set<string>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Check which files have local git changes (modified, staged, or untracked)
|
|
5
|
+
* Note: Checks the entire git repository, not just the working directory
|
|
6
|
+
* @param workingDir - The directory to check git status in (used to find git root)
|
|
7
|
+
* @returns Set of absolute file paths that have local changes
|
|
8
|
+
*/
|
|
9
|
+
export function checkGitStatus(workingDir) {
|
|
10
|
+
const changedFiles = new Set();
|
|
11
|
+
try {
|
|
12
|
+
// Get the git root directory first
|
|
13
|
+
// git status --porcelain returns paths relative to git root, not cwd
|
|
14
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
15
|
+
cwd: workingDir,
|
|
16
|
+
encoding: "utf-8",
|
|
17
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
18
|
+
}).trim();
|
|
19
|
+
// Run git status --porcelain to get list of changed files
|
|
20
|
+
// --porcelain gives machine-readable output
|
|
21
|
+
const output = execSync("git status --porcelain", {
|
|
22
|
+
cwd: workingDir,
|
|
23
|
+
encoding: "utf-8",
|
|
24
|
+
stdio: ["pipe", "pipe", "ignore"], // Ignore stderr to avoid errors when not in git repo
|
|
25
|
+
});
|
|
26
|
+
// Parse the output
|
|
27
|
+
// Format: "XY filename" where X is staged status, Y is unstaged status
|
|
28
|
+
const lines = output.trim().split("\n");
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
if (!line)
|
|
31
|
+
continue;
|
|
32
|
+
// Extract filename (skip first 3 characters which are status codes and space)
|
|
33
|
+
const filename = line.slice(3).trim();
|
|
34
|
+
// Handle renamed files (format: "old -> new")
|
|
35
|
+
const actualFilename = filename.includes(" -> ") ? filename.split(" -> ")[1] : filename;
|
|
36
|
+
if (actualFilename) {
|
|
37
|
+
// Git status returns paths relative to git root, not cwd
|
|
38
|
+
const absolutePath = path.resolve(gitRoot, actualFilename);
|
|
39
|
+
changedFiles.add(absolutePath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (_error) {
|
|
44
|
+
// If git command fails (e.g., not in a git repo), return empty set
|
|
45
|
+
// This allows the tool to work in non-git directories
|
|
46
|
+
return changedFiles;
|
|
47
|
+
}
|
|
48
|
+
return changedFiles;
|
|
49
|
+
}
|