Skip to content

Commit

Permalink
Add example of false positive with re-export involving index and * (#269
Browse files Browse the repository at this point in the history
)

Improve support of re-exports involving *. Improve output for such exports.

* Add example of false positive with re-export involving index and *
* Avoid false positives when a export * is partly imported at a higher level
* Add explicit null use
* Add more tests and nicer format if unused involves export *
* Add version bump to help when releasing
* 9.0.4
  • Loading branch information
mrseanryan authored Feb 12, 2023
1 parent 2a6315c commit 48eb6e7
Show file tree
Hide file tree
Showing 20 changed files with 242 additions and 11 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## [9.0.4] - 12 Feb 2023

### Changed

- Improve handling of transitive imports like 'a -> b -> c' especially where 'export *' is used - #216
- Improve output format when the unused item involves an 'export *'.

## [9.0.3] - 5 Feb 2023

### Changed
Expand Down
1 change: 1 addition & 0 deletions example/import-and-re-export-2/execute.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
../../bin/ts-unused-exports ./tsconfig.json
2 changes: 2 additions & 0 deletions example/import-and-re-export-2/src/a/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const A = 1;
export const A_2 = 1;
2 changes: 2 additions & 0 deletions example/import-and-re-export-2/src/b.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * as fromA from './a';
export const B_unused = 1;
2 changes: 2 additions & 0 deletions example/import-and-re-export-2/src/c.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { fromA } from './b';
export const C_unused = 1;
3 changes: 3 additions & 0 deletions example/import-and-re-export-2/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"include": ["src"]
}
1 change: 1 addition & 0 deletions example/import-and-re-export/execute.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
../../bin/ts-unused-exports ./tsconfig.json
2 changes: 2 additions & 0 deletions example/import-and-re-export/src/a/a/a.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const a = 0
export const unusedInInnerIndex = 0
1 change: 1 addition & 0 deletions example/import-and-re-export/src/a/a/a_unused.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const unusedInA = 0
1 change: 1 addition & 0 deletions example/import-and-re-export/src/a/a/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './a'
2 changes: 2 additions & 0 deletions example/import-and-re-export/src/a/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './a'

3 changes: 3 additions & 0 deletions example/import-and-re-export/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { a } from './a'

export const unusedInTopIndex = 0
3 changes: 3 additions & 0 deletions example/import-and-re-export/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"include": ["src"]
}
144 changes: 143 additions & 1 deletion features/export-statements-star.feature
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,148 @@ Scenario: export * from ./a/a.ts
When analyzing "tsconfig.json"
Then the result is { "b.ts": ["A_unused"] }

Scenario: export * from ./a/a/a.ts
Given file "a/a/a.ts" is
"""
export const A = 1;
export const A_innermost_unused = 1;
"""
And file "a/a/index.ts" is
"""
export * from "./a";
"""
And file "a/a.ts" is export * from './a';
And file "a/index.ts" is
"""
export * from "./a";
export const A_unused = 1;
"""
And file "c.ts" is import { A } from './a';

When analyzing "tsconfig.json"
# note A_innermost_unused is not detected - that would require parsing for all usages of the namespace
Then the result is { "a/index.ts": ["A_unused"] }

Scenario: export * from ./a1/a2/a.ts
Given file "a1/a2/a.ts" is
"""
export const A = 1;
export const A_innermost_unused = 1;
"""
And file "a1/a2/index.ts" is
"""
export * from "./a";
"""
And file "a1/a.ts" is export * from './a2';
And file "a1/index.ts" is
"""
export * from "./a";
export const A_unused = 1;
"""
And file "c.ts" is import { A } from './a1';

When analyzing "tsconfig.json"
# note A_innermost_unused is not detected - that would require parsing for all usages of the namespace
Then the result is { "a1/index.ts": ["A_unused"] }

Scenario: export * from ./a1/a2/a.ts - import skipping the mid-level - import from innermost index
Given file "a1/a2/a.ts" is
"""
export const A = 1;
export const A_innermost_unused = 1;
"""
And file "a1/a2/index.ts" is
"""
export * from "./a";
"""
# note this IS flagged as unused
And file "a1/a.ts" is export * from './a2';
And file "a1/index.ts" is
"""
// skips the mid-level file a1/a.ts
export * from "./a1/a2";
export const A_unused = 1;
"""
And file "c.ts" is import { A } from './a1';

When analyzing "tsconfig.json"
# note A_innermost_unused is NOT detected
Then the result is { "a1/a_ts":["* -> /a1/a2/a"], "a1/index.ts": ["A_unused"] }

Scenario: export * from ./a1/a2/a.ts - import skipping the mid-level - import from innermost a.ts
Given file "a1/a2/a.ts" is
"""
export const A = 1;
export const A_innermost_unused = 1;
"""
# TODO this could be flagged as unused
And file "a1/a2/index.ts" is
"""
export * from "./a";
"""
# TODO this could be flagged as unused
And file "a1/a.ts" is export * from './a';
And file "a1/index.ts" is
"""
// skips the mid-level - imports directly from a1/a2/a.ts NOT the index, so not from an export *
export * from "./a1/a2/a";
export const A_unused = 1;
"""
And file "c.ts" is import { A } from './a1';

When analyzing "tsconfig.json"
# note A_innermost_unused is NOT detected
# TODO review - A is flagged via "a1/a2/index_ts":["A"...] - which is strictly correct, although could be annoying
Then the result is { "a1/index.ts": ["A_unused"],"a1/a2/index_ts":["A","A_innermost_unused"] }

Scenario: export * from ./a/a/a.ts - import skipping the mid-level
Given file "a/a/a.ts" is
"""
export const A = 1;
export const A_innermost_unused = 1;
"""
And file "a/a/index.ts" is
# TODO this could be flagged as unused
"""
export * from "./a";
"""
And file "a/a.ts" is export * from './a';
And file "a/index.ts" is
"""
// skips the mid-level
export * from "./a/a";
export const A_unused = 1;
"""
And file "c.ts" is import { A } from './a';

When analyzing "tsconfig.json"
# note A_innermost_unused IS detected
Then the result is { "a/index.ts": ["A_unused", "A_innermost_unused"] }

Scenario: export * from ./a/a/a.ts - import skipping the mid-level
Given file "a/a/a.ts" is
"""
export const A = 1;
export const A_innermost_unused = 1;
"""
And file "a/a/index.ts" is
"""
export * from "./a";
"""
# TODO this could be flagged as unused
And file "a/a.ts" is export * from './a';
And file "a/index.ts" is
"""
// skips the mid-level - imports directly from a/a/a.ts NOT the index, so not from an export *
export * from "./a/a/a";
export const A_unused = 1;
"""
And file "c.ts" is import { A } from './a';

When analyzing "tsconfig.json"
# note A_innermost_unused is NOT detected
Then the result is { "a/index.ts": ["A_unused"] }

Scenario: export * from ./a
Given file "a/index.ts" is
"""
Expand All @@ -34,8 +176,8 @@ Scenario: export * as fromA from ./a/a.ts
And file "c.ts" is import { fromA } from './b';

When analyzing "tsconfig.json"
# note A_unused is not detected - that would require parsing for all usages of the namespace
Then the result is { }
# note A_unused is not detected - that would require parsing for all usages of the namespace

Scenario: export * as fromA from ./a/a.ts
Given file "a/a.ts" is
Expand Down
8 changes: 8 additions & 0 deletions ispec/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ pushd ../example/path-alias-and-sub-folders-import-from-index
run_itest
popd

pushd ../example/import-and-re-export
run_itest
popd

pushd ../example/import-and-re-export-2
run_itest
popd

pushd ../example/with-js
run_itest
popd
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ts-unused-exports",
"version": "9.0.3",
"version": "9.0.4",
"description": "ts-unused-exports finds unused exported symbols in your Typescript project",
"main": "lib/app.js",
"repository": {
Expand All @@ -21,6 +21,7 @@
},
"homepage": "https://github.com/pzavolinsky/ts-unused-exports",
"scripts": {
"bump": "npm version patch",
"build": "tsc",
"exec": "bin/ts-unused-exports",
"lint:fix": "./node_modules/.bin/eslint --ext .ts src --fix",
Expand Down
56 changes: 49 additions & 7 deletions src/analyzer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Analysis,
ExtraCommandLineOptions,
ExtraOptionsForPresentation,
File,
LocationInFile,
} from './types';
Expand All @@ -25,6 +26,8 @@ interface FileExports {
interface ExportItem {
exports: FileExports;
path: string;
// Was the file imported at least once. If so, then can mark any export * as used (handles transitive export(import) like a -> b -> c).
fileWasImported: boolean;
}

interface ExportMap {
Expand Down Expand Up @@ -61,7 +64,7 @@ const getFileExports = (file: File): ExportItem => {
}
});

return { exports, path: file.fullPath };
return { exports, path: file.fullPath, fileWasImported: false };
};

const getExportMap = (files: File[]): ExportMap => {
Expand All @@ -74,7 +77,10 @@ const getExportMap = (files: File[]): ExportMap => {

const processImports = (file: File, exportMap: ExportMap): void => {
Object.keys(file.imports).forEach((key) => {
let ex = exportMap[removeFileExtensionToAllowForJs(key)]?.exports;
const importedFileExports =
exportMap[removeFileExtensionToAllowForJs(key)] || null;

let ex = importedFileExports?.exports || null;

// Handle imports from an index file
if (!ex) {
Expand Down Expand Up @@ -121,9 +127,15 @@ const processImports = (file: File, exportMap: ExportMap): void => {
},
};
}

// DEV DEBUG - console.log(`Marking as used: ${imp} in ${file.path}`);
ex[imp].usageCount++;
};

if (!!importedFileExports) {
importedFileExports.fileWasImported = true;
}

file.imports[key].forEach((imp) => {
imp === '*' ? Object.keys(ex).forEach(addUsage) : addUsage(imp);
});
Expand Down Expand Up @@ -164,6 +176,12 @@ const expandExportFromStarOrStarAsForFile = (
// Mark the items as imported, for the imported file:
const importedFileExports = exportMap[removeExportStarPrefix(ex)];
if (importedFileExports) {
// DEV DEBUG
/* console.log(
`Marking as used: ${key} in ${removeExportStarPrefix(ex)}`,
);
console.dir(Object.keys(exportMap));*/
importedFileExports.fileWasImported = true;
importedFileExports.exports[key].usageCount++;
}
});
Expand Down Expand Up @@ -233,9 +251,24 @@ const areEqual = (files1: string[], files2: string[]): boolean => {
return files1.every((f) => files2.includes(f));
};

const makeExportStarRelativeForPresentation = (
baseUrl: string | undefined,
filePath: string,
): string => {
if (!filePath.startsWith('*')) {
return filePath;
}

const filePathNoStar = removeExportStarPrefix(filePath);
if (!!baseUrl && filePathNoStar.startsWith(baseUrl)) {
return `* -> ${filePathNoStar.substring(baseUrl.length)}`;
}
return filePath;
};

export default (
files: File[],
extraOptions?: ExtraCommandLineOptions,
extraOptions?: ExtraCommandLineOptions & ExtraOptionsForPresentation,
): Analysis => {
const filteredFiles = filterFiles(files, extraOptions);

Expand All @@ -252,9 +285,15 @@ export default (

if (shouldPathBeExcludedFromResults(path, extraOptions)) return;

const unusedExports = Object.keys(exports).filter(
(k) => exports[k].usageCount === 0,
);
const unusedExports = Object.keys(exports).filter((k) => {
// If the file was imported at least once, then do NOT consider any of its 'export (import) *' as unused.
// This avoids false positives with transitive import/exports like a -> b -> c.
if (expItem.fileWasImported && k.startsWith('*')) {
return false;
}

return exports[k].usageCount === 0;
});

if (unusedExports.length === 0) {
return;
Expand All @@ -265,7 +304,10 @@ export default (
analysis[path] = [];
unusedExports.forEach((e) => {
analysis[path].push({
exportName: e,
exportName: makeExportStarRelativeForPresentation(
extraOptions?.baseUrl,
e,
),
location: exports[e].location,
});
});
Expand Down
6 changes: 5 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,9 @@ export default (tsconfigPath: string, files?: string[]): Analysis => {
const args = extractOptionsFromFiles(files);
const tsConfig = loadTsConfig(tsconfigPath, args.tsFiles);

return analyze(parseFiles(tsConfig, args.options), args.options);
const options = {
...args.options,
baseUrl: tsConfig.baseUrl,
};
return analyze(parseFiles(tsConfig, args.options), options);
};
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,7 @@ export interface ExtraCommandLineOptions {
silent?: boolean;
findCompletelyUnusedFiles?: boolean;
}

export interface ExtraOptionsForPresentation {
baseUrl?: string;
}

0 comments on commit 48eb6e7

Please sign in to comment.