Skip to content

Commit

Permalink
Merge 418f2c6 into 9c7d4d6
Browse files Browse the repository at this point in the history
  • Loading branch information
mrseanryan committed Nov 23, 2019
2 parents 9c7d4d6 + 418f2c6 commit 346920b
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 172 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Added

- Detect dynamic imports, to avoid false reports of unused exports

### Changed

- (Internal) Update dependencies (except for TypeScript)
Expand Down
54 changes: 54 additions & 0 deletions features/include-dynamic-imports.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
Feature: include dynamic imports

Scenario: Include dynamic import as promise
Given file "a.ts" is
"""
export default type A = 1;
export type A_unused = 2;
"""
And file "b.ts" is
"""
import("./a").then(A_imported => {
console.log(A_imported);
});
export const B_unused: A = 0
"""
When analyzing "tsconfig.json"
Then the result is { "b.ts": ["B_unused"], "a.ts": ["A_unused"] }

# TODO xxx fix
Scenario: Include dynamic import as promise - in a function
Given file "a.ts" is
"""
export default type A = 1;
export type A_unused = 2;
"""
And file "b.ts" is
"""
function imports() {
import("./a").then(A_imported => {
console.log(A_imported);
});
}
export const B_unused: A = 0
"""
When analyzing "tsconfig.json"
Then the result is { "b.ts": ["B_unused"], "a.ts": ["A_unused"] }

# TODO xxx fix
Scenario: Include dynamic import via await - in a function,
Given file "a.ts" is
"""
export default type A = 1;
export type A_unused = 2;
"""
And file "b.ts" is
"""
async function imports() {
const A_imported = await import("./a");
console.log(A_imported);
}
export const B_unused: A = 0
"""
When analyzing "tsconfig.json"
Then the result is { "b.ts": ["B_unused"], "a.ts": ["A_unused"] }
25 changes: 25 additions & 0 deletions src/parser.comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as ts from 'typescript';

// Parse Comments (that can disable ts-unused-exports)

export const isNodeDisabledViaComment = (
node: ts.Node,
file: ts.SourceFile,
): boolean => {
const comments = ts.getLeadingCommentRanges(
file.getFullText(),
node.getFullStart(),
);

if (comments) {
const commentRange = comments[comments.length - 1];
const commentText = file
.getFullText()
.substring(commentRange.pos, commentRange.end);
if (commentText === '// ts-unused-exports:disable-next-line') {
return true;
}
}

return false;
};
16 changes: 16 additions & 0 deletions src/parser.common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as ts from 'typescript';

export const star = ['*'];

export interface FromWhat {
from: string;
what: string[];
}

export const TRIM_QUOTES = /^['"](.*)['"]$/;

export const getFromText = (moduleSpecifier: string): string =>
moduleSpecifier.replace(TRIM_QUOTES, '$1').replace(/\/index$/, '');

export const getFrom = (moduleSpecifier: ts.Expression): string =>
getFromText(moduleSpecifier.getText());
70 changes: 70 additions & 0 deletions src/parser.dynamic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as ts from 'typescript';
import { FromWhat, getFromText } from './parser.common';

// Parse Dynamic Imports

export const mayContainDynamicImports = (node: ts.Node): boolean =>
node.getText().indexOf('import(') > -1;

type WithExpression = ts.Node & {
expression: ts.Expression;
};

export function isWithExpression(node: ts.Node): node is WithExpression {
const myInterface = node as WithExpression;
return !!myInterface.expression;
}

type WithArguments = ts.Node & {
arguments: ts.NodeArray<ts.Expression>;
};

export function isWithArguments(node: ts.Node): node is WithArguments {
const myInterface = node as WithArguments;
return !!myInterface.arguments;
}

export const addDynamicImports = (
node: ts.Node,
addImport: (fw: FromWhat) => void,
): void => {
const addImportsInAnyExpression = (node: ts.Node): void => {
const getArgumentFrom = (node: ts.Node): string | undefined => {
if (isWithArguments(node)) {
return node.arguments[0].getText();
}
};

if (isWithExpression(node)) {
let expr = node;
while (isWithExpression(expr)) {
const newExpr = expr.expression;

if (newExpr.getText() === 'import') {
const importing = getArgumentFrom(expr);

if (!!importing) {
addImport({
from: getFromText(importing),
what: ['default'],
});
}
}

if (isWithExpression(newExpr)) {
expr = newExpr;
} else {
break;
}
}
}
};

const recurseIntoChildren = (next: ts.Node): void => {
addImportsInAnyExpression(next);

next.getChildren().forEach(recurseIntoChildren);
};

recurseIntoChildren(node);
};
61 changes: 61 additions & 0 deletions src/parser.export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as ts from 'typescript';

import { LocationInFile } from './types';
import { FromWhat, star, getFrom } from './parser.common';

// Parse Exports

export const extractExportStatement = (
decl: ts.ExportDeclaration,
): string[] => {
return decl.exportClause
? decl.exportClause.elements.map(e => (e.name || e.propertyName).text)
: [];
};

export const extractExportFromImport = (
decl: ts.ExportDeclaration,
moduleSpecifier: ts.Expression,
): FromWhat => {
const { exportClause } = decl;
const what = exportClause
? exportClause.elements.map(e => (e.propertyName || e.name).text)
: star;

return {
from: getFrom(moduleSpecifier),
what,
};
};

export const extractExport = (path: string, node: ts.Node): string => {
switch (node.kind) {
case ts.SyntaxKind.VariableStatement:
return (node as ts.VariableStatement).declarationList.declarations[0].name.getText();
case ts.SyntaxKind.FunctionDeclaration:
const { name } = node as ts.FunctionDeclaration;
return name ? name.text : 'default';
default: {
console.warn(`WARN: ${path}: unknown export node (kind:${node.kind})`);
break;
}
}
return '';
};

export const addExportCore = (
exportName: string,
file: ts.SourceFile,
node: ts.Node,
exportLocations: LocationInFile[],
exports: string[],
): void => {
exports.push(exportName);

const location = file.getLineAndCharacterOfPosition(node.getStart());

exportLocations.push({
line: location.line + 1,
character: location.character,
});
};
89 changes: 89 additions & 0 deletions src/parser.import.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { dirname, join, relative, resolve, sep } from 'path';
import { existsSync } from 'fs';
import * as tsconfigPaths from 'tsconfig-paths';
import * as ts from 'typescript';

import { getFrom, FromWhat, star } from './parser.common';
import { Imports } from './types';

// Parse Imports

const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];

const relativeTo = (rootDir: string, file: string, path: string): string =>
relative(rootDir, resolve(dirname(file), path));

const isRelativeToBaseDir = (baseDir: string, from: string): boolean =>
existsSync(resolve(baseDir, `${from}.js`)) ||
existsSync(resolve(baseDir, `${from}.ts`)) ||
existsSync(resolve(baseDir, `${from}.tsx`)) ||
existsSync(resolve(baseDir, from, 'index.js')) ||
existsSync(resolve(baseDir, from, 'index.ts')) ||
existsSync(resolve(baseDir, from, 'index.tsx'));

export const extractImport = (decl: ts.ImportDeclaration): FromWhat => {
const from = getFrom(decl.moduleSpecifier);
const { importClause } = decl;
if (!importClause)
return {
from,
what: star,
};

const { namedBindings } = importClause;
const importDefault = !!importClause.name ? ['default'] : [];
const importStar =
namedBindings && !!(namedBindings as ts.NamespaceImport).name ? star : [];
const importNames =
namedBindings && !importStar.length
? (namedBindings as ts.NamedImports).elements.map(
e => (e.propertyName || e.name).text,
)
: [];

return {
from,
what: importDefault.concat(importStar, importNames),
};
};

export const addImportCore = (
fw: FromWhat,
rootDir: string,
path: string,
imports: Imports,
tsconfigPathsMatcher?: tsconfigPaths.MatchPath,
baseDir?: string,
baseUrl?: string,
): string | undefined => {
const { from, what } = fw;

const getKey = (from: string): string | undefined => {
if (from[0] == '.') {
// An undefined return indicates the import is from 'index.ts' or similar == '.'
return relativeTo(rootDir, path, from) || '.';
} else if (baseDir && baseUrl) {
let matchedPath;

return isRelativeToBaseDir(baseDir, from)
? baseUrl && join(baseUrl, from)
: tsconfigPathsMatcher &&
(matchedPath = tsconfigPathsMatcher(
from,
undefined,
undefined,
EXTENSIONS,
))
? matchedPath.replace(`${baseDir}${sep}`, '')
: undefined;
}

return undefined;
};

const key = getKey(from);
if (!key) return undefined;
const items = imports[key] || [];
imports[key] = items.concat(what);
return key;
};

0 comments on commit 346920b

Please sign in to comment.