Skip to content

Commit 4afb05a

Browse files
committed
feat: detect empty describe
1 parent 7285375 commit 4afb05a

File tree

10 files changed

+147
-7
lines changed

10 files changed

+147
-7
lines changed

detector/src/languages/TypescriptSmells.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as ts from 'typescript';
33
import { Smell, SmellsFinder } from "../types";
44
import { SmellsBuilder } from '../smells-builder';
55

6+
67
export class TypescriptSmells implements SmellsFinder {
78

89
constructor(private readonly ast: ts.SourceFile) { }
@@ -110,12 +111,15 @@ export class TypescriptSmells implements SmellsFinder {
110111
);
111112
}
112113

114+
const emptyDescribe = this.findEmptyDescribes(ast);
115+
113116
const result = ifs.concat(forOfs)
114117
.concat(forIns)
115118
.concat(fors)
116119
.concat(timeouts)
117120
.concat(consoles)
118-
.concat(jestMockSmells);
121+
.concat(jestMockSmells)
122+
.concat(emptyDescribe);
119123
return result;
120124
}
121125

@@ -215,4 +219,36 @@ export class TypescriptSmells implements SmellsFinder {
215219
});
216220
return functionCalls;
217221
}
222+
223+
private findEmptyDescribes(sourceFile: ts.SourceFile): Smell[] {
224+
const emptyDescribes: Smell[] = [];
225+
226+
function traverse(node: ts.Node) {
227+
if (ts.isCallExpression(node)) {
228+
const expression = node.expression;
229+
const isDescribeCall = ts.isIdentifier(expression) && expression.text === 'describe';
230+
if (isDescribeCall && node.arguments.length === 2) {
231+
const secondArg = node.arguments[1];
232+
if (ts.isArrowFunction(secondArg) || ts.isFunctionExpression(secondArg)) {
233+
const body = secondArg.body;
234+
if (ts.isBlock(body) && body.statements.length === 0) {
235+
const { line, character: startAt } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
236+
const { line: lineEnd, character: endsAt } = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
237+
emptyDescribes.push(SmellsBuilder.emptyDescribe(
238+
line + 1,
239+
lineEnd + 1,
240+
startAt,
241+
endsAt,
242+
));
243+
}
244+
}
245+
}
246+
}
247+
248+
ts.forEachChild(node, traverse);
249+
}
250+
251+
traverse(sourceFile);
252+
return emptyDescribes;
253+
}
218254
}

detector/src/smells-builder.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,23 @@ export class SmellsBuilder {
118118
description: `Smelly: Avoid mocking too many dependencies in the test file. Split the test cases to distribute the mocking load.`,
119119
diagnostic: `Smelly: Avoid mocking too many dependencies in the test file. Split the test cases to distribute the mocking load.`,
120120
};
121+
}
122+
123+
public static emptyDescribe(
124+
lineStart: number,
125+
lineEnd: number,
126+
startAt: number,
127+
endsAt: number
128+
): Smell {
129+
return {
130+
type: SmellType.emptyDescribe,
131+
lineStart,
132+
lineEnd,
133+
startAt,
134+
endsAt,
135+
description: 'Smelly: avoid empty test cases.',
136+
diagnostic: 'Smelly: avoid empty test cases.',
137+
};
121138

122139
}
123140
}

detector/src/smells-detector.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,15 @@ export class SmellDetector {
2626
testCases.push(...foundItEachCalls);
2727
testCases.push(...this.findItSkipCalls(ast));
2828

29+
const smells = new TypescriptSmells(ast).searchSmells();
30+
2931
const smellsList = {
3032
fileName: this.fileName,
3133
fileContent: this.code,
32-
smells: new TypescriptSmells(ast).searchSmells(), language
34+
smells,
35+
language
3336
};
37+
3438
return { smellsList, testCases };
3539
}
3640

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import ts from 'typescript';
2+
import { Smell, TestCase } from '../types';
3+
4+
export class FindDuplicatedTestCases {
5+
findTestCases(sourceFile: ts.SourceFile): TestCase[] {
6+
const testCases: TestCase[] = [];
7+
8+
function traverse(node: ts.Node) {
9+
if (ts.isCallExpression(node)) {
10+
const expression = node.expression;
11+
const isTestCall = ts.isIdentifier(expression) && (expression.text === 'it' || expression.text === 'test');
12+
if (isTestCall && node.arguments.length > 1) {
13+
const firstArg = node.arguments[0];
14+
const secondArg = node.arguments[1];
15+
if (ts.isStringLiteral(firstArg) && ts.isFunctionLike(secondArg)) {
16+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
17+
const { line: lineEnd, character: endsAt } = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
18+
// const bodyText = secondArg.getText(sourceFile);
19+
testCases.push({
20+
lineStart: line + 1,
21+
lineEnd: lineEnd + 1,
22+
startAt: character,
23+
endsAt: endsAt,
24+
});
25+
}
26+
}
27+
}
28+
29+
ts.forEachChild(node, traverse);
30+
}
31+
32+
traverse(sourceFile);
33+
return testCases;
34+
}
35+
36+
detectDuplicateTestCases(testCases: TestCase[]): Smell[] {
37+
const seen = new Map<string, TestCase[]>();
38+
const duplicates: Smell[] = [];
39+
40+
for (const testCase of testCases) {
41+
const key = `${testCase.lineStart}:${testCase.lineEnd}`;
42+
if (!seen.has(key)) {
43+
seen.set(key, []);
44+
}
45+
seen.get(key)!.push(testCase);
46+
}
47+
48+
for (const [key, cases] of seen.entries()) {
49+
if (cases.length > 1) {
50+
duplicates.push({
51+
type: 'duplicated-test-case',
52+
description: `Duplicated test case: ${key}`,
53+
diagnostic: `Duplicated test case: ${key}`,
54+
lineStart: cases[0].lineStart,
55+
lineEnd: cases[0].lineEnd,
56+
startAt: cases[0].startAt,
57+
endsAt: cases[0].endsAt,
58+
});
59+
}
60+
}
61+
62+
return duplicates;
63+
}
64+
}

detector/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ export enum SmellType {
4848
timeOut = 'timeout',
4949
consoleStatement = "console-statement",
5050
jestMock = "excessive-jest-mock",
51+
emptyDescribe = "empty-describe",
5152
}

detector/test/smells-detector-builder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const FOR = 'for-statement';
88
export const TIMEOUT = 'timeout';
99
export const CONSOLE = 'console-statement';
1010
export const MOCKERY = 'excessive-jest-mock';
11+
export const EMPTY_DESCRIBE = 'empty-describe';
1112

1213
export const JAVASCRIPT_FILE = 'javascript.js';
1314
export const TYPESCRIPT_FILE = 'javascript.ts';

detector/test/smells-detector-with-smells.test.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test } from 'vitest';
2-
import { CONSOLE, FOR, FOR_IN, FOR_OF, IF_STATEMENT, MOCKERY, smellDetectorInstance, TIMEOUT, totalTestCaseDetectorInstance, JAVASCRIPT_FILE, TYPESCRIPT_FILE } from './smells-detector-builder';
2+
import { CONSOLE, FOR, FOR_IN, FOR_OF, IF_STATEMENT, MOCKERY, smellDetectorInstance, TIMEOUT, totalTestCaseDetectorInstance, JAVASCRIPT_FILE, TYPESCRIPT_FILE, EMPTY_DESCRIBE } from './smells-detector-builder';
33

44
describe('Smelly Test Smell Detection Suite', () => {
55
describe.each([[{
@@ -358,9 +358,21 @@ jest.mock("../");`,
358358
total: 1,
359359
description: `Smelly: Avoid mocking too many dependencies in the test file. Split the test cases to distribute the mocking load.`,
360360
diagnostic: `Smelly: Avoid mocking too many dependencies in the test file. Split the test cases to distribute the mocking load.`,
361+
}],
362+
[{
363+
code: `describe('test', () => {})`,
364+
fileName: JAVASCRIPT_FILE,
365+
index: 0,
366+
type: EMPTY_DESCRIBE,
367+
lineStart: 1,
368+
lineEnd: 1,
369+
startAt: 0,
370+
endsAt: 26,
371+
total: 1,
372+
description: `Smelly: avoid empty test cases.`,
373+
diagnostic: `Smelly: avoid empty test cases.`,
361374
}]
362375
])(`detect test smell for %s %s: type %s %s at index %s`, ({ code, fileName, index, type, lineStart, lineEnd, startAt, endsAt, total, description, diagnostic }) => {
363-
364376
test(`should find ${total} test smells`, () => {
365377
const result = smellDetectorInstance(code, fileName);
366378

run-report.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
clear && \
44
npm run build && \
5-
rm smelly-report.html && \
6-
npm run cli -w cli -- "$1" typescript --report=html --report-output=$(pwd) && \
5+
rm -rf smelly-report.html && \
6+
npm run cli -w cli -- "$1" --report=html --report-output=$(pwd) && \
77
open smelly-report.html
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
describe("my test", () => {
2+
it("a", () => {
3+
expect(1).toBe(1);
4+
});
5+
});

vscode/src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ let currentDecoration: vscode.TextEditorDecorationType = warningDecorationType;
1313
let ranges: ComposedSmell[] = [];
1414
let hovers: vscode.Disposable[] = [];
1515
let collection: vscode.DiagnosticCollection;
16-
let smellyStatusBar: vscode.StatusBarItem;
16+
export let smellyStatusBar: vscode.StatusBarItem;
1717

1818
function fetchConfiguration(): SmellyConfiguration {
1919
return vscode.workspace.getConfiguration().get<SmellyConfiguration>(EXTENSION_IDENTIFIER) || {};

0 commit comments

Comments
 (0)