react-code-smell-detector 1.1.1 → 1.3.1

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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -11
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/analyzer.js +65 -2
  5. package/dist/cli.js +134 -33
  6. package/dist/detectors/accessibility.d.ts +12 -0
  7. package/dist/detectors/accessibility.d.ts.map +1 -0
  8. package/dist/detectors/accessibility.js +191 -0
  9. package/dist/detectors/complexity.d.ts +17 -0
  10. package/dist/detectors/complexity.d.ts.map +1 -0
  11. package/dist/detectors/complexity.js +69 -0
  12. package/dist/detectors/debug.d.ts +10 -0
  13. package/dist/detectors/debug.d.ts.map +1 -0
  14. package/dist/detectors/debug.js +87 -0
  15. package/dist/detectors/imports.d.ts +22 -0
  16. package/dist/detectors/imports.d.ts.map +1 -0
  17. package/dist/detectors/imports.js +210 -0
  18. package/dist/detectors/index.d.ts +6 -0
  19. package/dist/detectors/index.d.ts.map +1 -1
  20. package/dist/detectors/index.js +8 -0
  21. package/dist/detectors/memoryLeak.d.ts +7 -0
  22. package/dist/detectors/memoryLeak.d.ts.map +1 -0
  23. package/dist/detectors/memoryLeak.js +111 -0
  24. package/dist/detectors/security.d.ts +12 -0
  25. package/dist/detectors/security.d.ts.map +1 -0
  26. package/dist/detectors/security.js +161 -0
  27. package/dist/fixer.d.ts +23 -0
  28. package/dist/fixer.d.ts.map +1 -0
  29. package/dist/fixer.js +133 -0
  30. package/dist/git.d.ts +28 -0
  31. package/dist/git.d.ts.map +1 -0
  32. package/dist/git.js +117 -0
  33. package/dist/htmlReporter.d.ts +6 -0
  34. package/dist/htmlReporter.d.ts.map +1 -0
  35. package/dist/htmlReporter.js +453 -0
  36. package/dist/reporter.js +26 -0
  37. package/dist/types/index.d.ts +10 -1
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/index.js +13 -0
  40. package/dist/watcher.d.ts +16 -0
  41. package/dist/watcher.d.ts.map +1 -0
  42. package/dist/watcher.js +89 -0
  43. package/package.json +8 -2
  44. package/src/analyzer.ts +0 -269
  45. package/src/cli.ts +0 -125
  46. package/src/detectors/deadCode.ts +0 -163
  47. package/src/detectors/dependencyArray.ts +0 -176
  48. package/src/detectors/hooksRules.ts +0 -101
  49. package/src/detectors/index.ts +0 -16
  50. package/src/detectors/javascript.ts +0 -169
  51. package/src/detectors/largeComponent.ts +0 -63
  52. package/src/detectors/magicValues.ts +0 -114
  53. package/src/detectors/memoization.ts +0 -177
  54. package/src/detectors/missingKey.ts +0 -105
  55. package/src/detectors/nestedTernary.ts +0 -75
  56. package/src/detectors/nextjs.ts +0 -124
  57. package/src/detectors/nodejs.ts +0 -199
  58. package/src/detectors/propDrilling.ts +0 -103
  59. package/src/detectors/reactNative.ts +0 -154
  60. package/src/detectors/typescript.ts +0 -151
  61. package/src/detectors/useEffect.ts +0 -117
  62. package/src/index.ts +0 -4
  63. package/src/parser/index.ts +0 -195
  64. package/src/reporter.ts +0 -278
  65. package/src/types/index.ts +0 -144
  66. package/tsconfig.json +0 -19
package/src/analyzer.ts DELETED
@@ -1,269 +0,0 @@
1
- import fg from 'fast-glob';
2
- import path from 'path';
3
- import { parseFile, ParseResult } from './parser/index.js';
4
- import {
5
- detectUseEffectOveruse,
6
- detectPropDrilling,
7
- analyzePropDrillingDepth,
8
- detectLargeComponent,
9
- detectUnmemoizedCalculations,
10
- detectMissingKeys,
11
- detectHooksRulesViolations,
12
- detectDependencyArrayIssues,
13
- detectNestedTernaries,
14
- detectDeadCode,
15
- detectMagicValues,
16
- detectNextjsIssues,
17
- detectReactNativeIssues,
18
- detectNodejsIssues,
19
- detectJavascriptIssues,
20
- detectTypescriptIssues,
21
- } from './detectors/index.js';
22
- import {
23
- AnalysisResult,
24
- FileAnalysis,
25
- ComponentInfo,
26
- CodeSmell,
27
- AnalysisSummary,
28
- TechnicalDebtScore,
29
- DetectorConfig,
30
- DEFAULT_CONFIG,
31
- SmellType,
32
- SmellSeverity,
33
- } from './types/index.js';
34
-
35
- export interface AnalyzerOptions {
36
- rootDir: string;
37
- include?: string[];
38
- exclude?: string[];
39
- config?: Partial<DetectorConfig>;
40
- }
41
-
42
- export async function analyzeProject(options: AnalyzerOptions): Promise<AnalysisResult> {
43
- const {
44
- rootDir,
45
- include = ['**/*.tsx', '**/*.jsx'],
46
- exclude = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.test.*', '**/*.spec.*'],
47
- config: userConfig = {},
48
- } = options;
49
-
50
- const config: DetectorConfig = { ...DEFAULT_CONFIG, ...userConfig };
51
-
52
- // Find all React files
53
- const patterns = include.map(p => path.join(rootDir, p));
54
- const files = await fg(patterns, {
55
- ignore: exclude,
56
- absolute: true,
57
- });
58
-
59
- const fileAnalyses: FileAnalysis[] = [];
60
-
61
- // Analyze each file
62
- for (const file of files) {
63
- try {
64
- const parseResult = await parseFile(file);
65
- const analysis = analyzeFile(parseResult, file, config);
66
- if (analysis.components.length > 0 || analysis.smells.length > 0) {
67
- fileAnalyses.push(analysis);
68
- }
69
- } catch (error) {
70
- // Skip files that can't be parsed
71
- console.warn(`Warning: Could not parse ${file}: ${(error as Error).message}`);
72
- }
73
- }
74
-
75
- // Calculate summary and score
76
- const summary = calculateSummary(fileAnalyses);
77
- const debtScore = calculateTechnicalDebtScore(fileAnalyses, summary);
78
-
79
- return {
80
- files: fileAnalyses,
81
- summary,
82
- debtScore,
83
- };
84
- }
85
-
86
- function analyzeFile(parseResult: ParseResult, filePath: string, config: DetectorConfig): FileAnalysis {
87
- const { components, imports, sourceCode } = parseResult;
88
- const smells: CodeSmell[] = [];
89
- const componentInfos: ComponentInfo[] = [];
90
-
91
- // Run all detectors on each component
92
- components.forEach(component => {
93
- // Collect component info
94
- componentInfos.push({
95
- name: component.name,
96
- file: filePath,
97
- startLine: component.startLine,
98
- endLine: component.endLine,
99
- lineCount: component.endLine - component.startLine + 1,
100
- useEffectCount: component.hooks.useEffect.length,
101
- useStateCount: component.hooks.useState.length,
102
- useMemoCount: component.hooks.useMemo.length,
103
- useCallbackCount: component.hooks.useCallback.length,
104
- propsCount: component.props.length,
105
- propsDrillingDepth: 0, // Calculated separately
106
- hasExpensiveCalculation: false, // Will be set by memoization detector
107
- });
108
-
109
- // Run detectors
110
- smells.push(...detectUseEffectOveruse(component, filePath, sourceCode, config));
111
- smells.push(...detectPropDrilling(component, filePath, sourceCode, config));
112
- smells.push(...detectLargeComponent(component, filePath, sourceCode, config));
113
- smells.push(...detectUnmemoizedCalculations(component, filePath, sourceCode, config));
114
- smells.push(...detectMissingKeys(component, filePath, sourceCode, config));
115
- smells.push(...detectHooksRulesViolations(component, filePath, sourceCode, config));
116
- smells.push(...detectDependencyArrayIssues(component, filePath, sourceCode, config));
117
- smells.push(...detectNestedTernaries(component, filePath, sourceCode, config));
118
- smells.push(...detectDeadCode(component, filePath, sourceCode, config));
119
- smells.push(...detectMagicValues(component, filePath, sourceCode, config));
120
- // Framework-specific detectors
121
- smells.push(...detectNextjsIssues(component, filePath, sourceCode, config, imports));
122
- smells.push(...detectReactNativeIssues(component, filePath, sourceCode, config, imports));
123
- smells.push(...detectNodejsIssues(component, filePath, sourceCode, config, imports));
124
- smells.push(...detectJavascriptIssues(component, filePath, sourceCode, config));
125
- smells.push(...detectTypescriptIssues(component, filePath, sourceCode, config));
126
- });
127
-
128
- // Run cross-component analysis
129
- smells.push(...analyzePropDrillingDepth(components, filePath, sourceCode, config));
130
-
131
- return {
132
- file: filePath,
133
- components: componentInfos,
134
- smells,
135
- imports,
136
- };
137
- }
138
-
139
- function calculateSummary(files: FileAnalysis[]): AnalysisSummary {
140
- const smellsByType: Record<SmellType, number> = {
141
- 'useEffect-overuse': 0,
142
- 'prop-drilling': 0,
143
- 'large-component': 0,
144
- 'unmemoized-calculation': 0,
145
- 'missing-dependency': 0,
146
- 'state-in-loop': 0,
147
- 'inline-function-prop': 0,
148
- 'deep-nesting': 0,
149
- 'missing-key': 0,
150
- 'hooks-rules-violation': 0,
151
- 'dependency-array-issue': 0,
152
- 'nested-ternary': 0,
153
- 'dead-code': 0,
154
- 'magic-value': 0,
155
- // Next.js
156
- 'nextjs-client-server-boundary': 0,
157
- 'nextjs-missing-metadata': 0,
158
- 'nextjs-image-unoptimized': 0,
159
- 'nextjs-router-misuse': 0,
160
- // React Native
161
- 'rn-inline-style': 0,
162
- 'rn-missing-accessibility': 0,
163
- 'rn-performance-issue': 0,
164
- // Node.js
165
- 'nodejs-callback-hell': 0,
166
- 'nodejs-unhandled-promise': 0,
167
- 'nodejs-sync-io': 0,
168
- 'nodejs-missing-error-handling': 0,
169
- // JavaScript
170
- 'js-var-usage': 0,
171
- 'js-loose-equality': 0,
172
- 'js-implicit-coercion': 0,
173
- 'js-global-pollution': 0,
174
- // TypeScript
175
- 'ts-any-usage': 0,
176
- 'ts-missing-return-type': 0,
177
- 'ts-non-null-assertion': 0,
178
- 'ts-type-assertion': 0,
179
- };
180
-
181
- const smellsBySeverity: Record<SmellSeverity, number> = {
182
- error: 0,
183
- warning: 0,
184
- info: 0,
185
- };
186
-
187
- let totalSmells = 0;
188
- let totalComponents = 0;
189
-
190
- files.forEach(file => {
191
- totalComponents += file.components.length;
192
- file.smells.forEach(smell => {
193
- totalSmells++;
194
- smellsByType[smell.type]++;
195
- smellsBySeverity[smell.severity]++;
196
- });
197
- });
198
-
199
- return {
200
- totalFiles: files.length,
201
- totalComponents,
202
- totalSmells,
203
- smellsByType,
204
- smellsBySeverity,
205
- };
206
- }
207
-
208
- function calculateTechnicalDebtScore(files: FileAnalysis[], summary: AnalysisSummary): TechnicalDebtScore {
209
- // Calculate individual scores (0-100, higher is better)
210
-
211
- // useEffect score: penalize based on useEffect-related issues
212
- const useEffectIssues = summary.smellsByType['useEffect-overuse'];
213
- const useEffectScore = Math.max(0, 100 - useEffectIssues * 15);
214
-
215
- // Prop drilling score
216
- const propDrillingIssues = summary.smellsByType['prop-drilling'];
217
- const propDrillingScore = Math.max(0, 100 - propDrillingIssues * 12);
218
-
219
- // Component size score
220
- const sizeIssues = summary.smellsByType['large-component'] + summary.smellsByType['deep-nesting'];
221
- const componentSizeScore = Math.max(0, 100 - sizeIssues * 10);
222
-
223
- // Memoization score
224
- const memoIssues = summary.smellsByType['unmemoized-calculation'] + summary.smellsByType['inline-function-prop'];
225
- const memoizationScore = Math.max(0, 100 - memoIssues * 8);
226
-
227
- // Overall score (weighted average)
228
- const score = Math.round(
229
- useEffectScore * 0.3 +
230
- propDrillingScore * 0.25 +
231
- componentSizeScore * 0.25 +
232
- memoizationScore * 0.2
233
- );
234
-
235
- // Determine grade
236
- let grade: 'A' | 'B' | 'C' | 'D' | 'F';
237
- if (score >= 90) grade = 'A';
238
- else if (score >= 80) grade = 'B';
239
- else if (score >= 70) grade = 'C';
240
- else if (score >= 60) grade = 'D';
241
- else grade = 'F';
242
-
243
- // Estimate refactor time
244
- const errorCount = summary.smellsBySeverity.error;
245
- const warningCount = summary.smellsBySeverity.warning;
246
- const totalIssues = errorCount * 30 + warningCount * 15; // minutes
247
-
248
- let estimatedRefactorTime: string;
249
- if (totalIssues < 30) estimatedRefactorTime = '< 30 minutes';
250
- else if (totalIssues < 60) estimatedRefactorTime = '30 min - 1 hour';
251
- else if (totalIssues < 120) estimatedRefactorTime = '1-2 hours';
252
- else if (totalIssues < 240) estimatedRefactorTime = '2-4 hours';
253
- else if (totalIssues < 480) estimatedRefactorTime = '4-8 hours';
254
- else estimatedRefactorTime = '> 1 day';
255
-
256
- return {
257
- score,
258
- grade,
259
- breakdown: {
260
- useEffectScore,
261
- propDrillingScore,
262
- componentSizeScore,
263
- memoizationScore,
264
- },
265
- estimatedRefactorTime,
266
- };
267
- }
268
-
269
- export { DEFAULT_CONFIG, DetectorConfig } from './types/index.js';
package/src/cli.ts DELETED
@@ -1,125 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from 'commander';
4
- import chalk from 'chalk';
5
- import ora from 'ora';
6
- import path from 'path';
7
- import { analyzeProject, DEFAULT_CONFIG } from './analyzer.js';
8
- import { reportResults } from './reporter.js';
9
- import fs from 'fs/promises';
10
-
11
- const program = new Command();
12
-
13
- program
14
- .name('react-smell')
15
- .description('Detect code smells in React projects')
16
- .version('1.0.0')
17
- .argument('[directory]', 'Directory to analyze', '.')
18
- .option('-f, --format <format>', 'Output format: console, json, markdown', 'console')
19
- .option('-s, --snippets', 'Show code snippets in output', false)
20
- .option('-c, --config <file>', 'Path to config file')
21
- .option('--max-effects <number>', 'Max useEffects per component', parseInt)
22
- .option('--max-props <number>', 'Max props before warning', parseInt)
23
- .option('--max-lines <number>', 'Max lines per component', parseInt)
24
- .option('--include <patterns>', 'Glob patterns to include (comma-separated)')
25
- .option('--exclude <patterns>', 'Glob patterns to exclude (comma-separated)')
26
- .option('-o, --output <file>', 'Write output to file')
27
- .action(async (directory, options) => {
28
- const rootDir = path.resolve(process.cwd(), directory);
29
-
30
- // Check if directory exists
31
- try {
32
- await fs.access(rootDir);
33
- } catch {
34
- console.error(chalk.red(`Error: Directory "${rootDir}" does not exist.`));
35
- process.exit(1);
36
- }
37
-
38
- const spinner = ora('Analyzing React project...').start();
39
-
40
- try {
41
- // Load config file if specified
42
- let fileConfig = {};
43
- if (options.config) {
44
- try {
45
- const configPath = path.resolve(process.cwd(), options.config);
46
- const configContent = await fs.readFile(configPath, 'utf-8');
47
- fileConfig = JSON.parse(configContent);
48
- } catch (error) {
49
- spinner.fail(`Could not load config file: ${(error as Error).message}`);
50
- process.exit(1);
51
- }
52
- }
53
-
54
- // Build config from options
55
- const config = {
56
- ...DEFAULT_CONFIG,
57
- ...fileConfig,
58
- ...(options.maxEffects && { maxUseEffectsPerComponent: options.maxEffects }),
59
- ...(options.maxProps && { maxPropsCount: options.maxProps }),
60
- ...(options.maxLines && { maxComponentLines: options.maxLines }),
61
- };
62
-
63
- const include = options.include?.split(',').map((p: string) => p.trim()) || undefined;
64
- const exclude = options.exclude?.split(',').map((p: string) => p.trim()) || undefined;
65
-
66
- const result = await analyzeProject({
67
- rootDir,
68
- include,
69
- exclude,
70
- config,
71
- });
72
-
73
- spinner.stop();
74
-
75
- const output = reportResults(result, {
76
- format: options.format,
77
- showCodeSnippets: options.snippets,
78
- rootDir,
79
- });
80
-
81
- if (options.output) {
82
- const outputPath = path.resolve(process.cwd(), options.output);
83
- await fs.writeFile(outputPath, output, 'utf-8');
84
- console.log(chalk.green(`✓ Report written to ${outputPath}`));
85
- } else {
86
- console.log(output);
87
- }
88
-
89
- // Exit with error code if there are errors
90
- if (result.summary.smellsBySeverity.error > 0) {
91
- process.exit(1);
92
- }
93
- } catch (error) {
94
- spinner.fail(`Analysis failed: ${(error as Error).message}`);
95
- if (process.env.DEBUG) {
96
- console.error(error);
97
- }
98
- process.exit(1);
99
- }
100
- });
101
-
102
- // Init command to create config file
103
- program
104
- .command('init')
105
- .description('Create a configuration file')
106
- .action(async () => {
107
- const configContent = JSON.stringify({
108
- maxUseEffectsPerComponent: DEFAULT_CONFIG.maxUseEffectsPerComponent,
109
- maxPropDrillingDepth: DEFAULT_CONFIG.maxPropDrillingDepth,
110
- maxComponentLines: DEFAULT_CONFIG.maxComponentLines,
111
- maxPropsCount: DEFAULT_CONFIG.maxPropsCount,
112
- }, null, 2);
113
-
114
- const configPath = path.join(process.cwd(), '.smellrc.json');
115
-
116
- try {
117
- await fs.access(configPath);
118
- console.log(chalk.yellow('Config file already exists at .smellrc.json'));
119
- } catch {
120
- await fs.writeFile(configPath, configContent, 'utf-8');
121
- console.log(chalk.green('✓ Created .smellrc.json'));
122
- }
123
- });
124
-
125
- program.parse();
@@ -1,163 +0,0 @@
1
- import * as t from '@babel/types';
2
- import { ParsedComponent, getCodeSnippet } from '../parser/index.js';
3
- import { CodeSmell, DetectorConfig, DEFAULT_CONFIG } from '../types/index.js';
4
-
5
- /**
6
- * Detects potentially dead code: unused variables, imports, and functions
7
- */
8
- export function detectDeadCode(
9
- component: ParsedComponent,
10
- filePath: string,
11
- sourceCode: string,
12
- config: DetectorConfig = DEFAULT_CONFIG
13
- ): CodeSmell[] {
14
- if (!config.checkDeadCode) return [];
15
-
16
- const smells: CodeSmell[] = [];
17
-
18
- // Track declared and used identifiers within the component
19
- const declared = new Map<string, { line: number; column: number; type: string }>();
20
- const used = new Set<string>();
21
-
22
- // Collect all declared variables in the component
23
- component.path.traverse({
24
- VariableDeclarator(path) {
25
- if (t.isIdentifier(path.node.id)) {
26
- const loc = path.node.loc;
27
- declared.set(path.node.id.name, {
28
- line: loc?.start.line || 0,
29
- column: loc?.start.column || 0,
30
- type: 'variable',
31
- });
32
- } else if (t.isObjectPattern(path.node.id)) {
33
- // Destructured variables
34
- path.node.id.properties.forEach(prop => {
35
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
36
- const loc = prop.loc;
37
- declared.set(prop.value.name, {
38
- line: loc?.start.line || 0,
39
- column: loc?.start.column || 0,
40
- type: 'variable',
41
- });
42
- } else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
43
- const loc = prop.loc;
44
- declared.set(prop.argument.name, {
45
- line: loc?.start.line || 0,
46
- column: loc?.start.column || 0,
47
- type: 'variable',
48
- });
49
- }
50
- });
51
- } else if (t.isArrayPattern(path.node.id)) {
52
- // Array destructured variables
53
- path.node.id.elements.forEach(elem => {
54
- if (t.isIdentifier(elem)) {
55
- const loc = elem.loc;
56
- declared.set(elem.name, {
57
- line: loc?.start.line || 0,
58
- column: loc?.start.column || 0,
59
- type: 'variable',
60
- });
61
- }
62
- });
63
- }
64
- },
65
-
66
- FunctionDeclaration(path) {
67
- if (path.node.id) {
68
- const loc = path.node.loc;
69
- declared.set(path.node.id.name, {
70
- line: loc?.start.line || 0,
71
- column: loc?.start.column || 0,
72
- type: 'function',
73
- });
74
- }
75
- },
76
- });
77
-
78
- // Collect all used identifiers
79
- component.path.traverse({
80
- Identifier(path) {
81
- // Skip if it's a declaration
82
- if (
83
- t.isVariableDeclarator(path.parent) && path.parent.id === path.node ||
84
- t.isFunctionDeclaration(path.parent) && path.parent.id === path.node ||
85
- t.isObjectProperty(path.parent) && path.parent.key === path.node ||
86
- t.isImportSpecifier(path.parent) ||
87
- t.isImportDefaultSpecifier(path.parent)
88
- ) {
89
- return;
90
- }
91
-
92
- // Skip property access (x.y - y is not a reference)
93
- if (t.isMemberExpression(path.parent) && path.parent.property === path.node && !path.parent.computed) {
94
- return;
95
- }
96
-
97
- used.add(path.node.name);
98
- },
99
-
100
- JSXIdentifier(path) {
101
- // Components used in JSX
102
- if (t.isJSXOpeningElement(path.parent) || t.isJSXClosingElement(path.parent)) {
103
- used.add(path.node.name);
104
- }
105
- },
106
- });
107
-
108
- // Find unused declarations
109
- declared.forEach((info, name) => {
110
- // Skip hooks and common patterns
111
- if (name.startsWith('_') || name.startsWith('set')) return;
112
-
113
- // Skip if it's the component name itself
114
- if (name === component.name) return;
115
-
116
- if (!used.has(name)) {
117
- smells.push({
118
- type: 'dead-code',
119
- severity: 'info',
120
- message: `Unused ${info.type} "${name}" in "${component.name}"`,
121
- file: filePath,
122
- line: info.line,
123
- column: info.column,
124
- suggestion: `Remove the unused ${info.type} or use it in the component.`,
125
- codeSnippet: getCodeSnippet(sourceCode, info.line),
126
- });
127
- }
128
- });
129
-
130
- return smells;
131
- }
132
-
133
- /**
134
- * Detects unused imports at the file level
135
- */
136
- export function detectUnusedImports(
137
- imports: Map<string, { source: string; line: number }>,
138
- usedInFile: Set<string>,
139
- filePath: string,
140
- sourceCode: string
141
- ): CodeSmell[] {
142
- const smells: CodeSmell[] = [];
143
-
144
- imports.forEach((info, name) => {
145
- // Skip React imports as they might be used implicitly
146
- if (name === 'React' || info.source === 'react') return;
147
-
148
- if (!usedInFile.has(name)) {
149
- smells.push({
150
- type: 'dead-code',
151
- severity: 'info',
152
- message: `Unused import "${name}" from "${info.source}"`,
153
- file: filePath,
154
- line: info.line,
155
- column: 0,
156
- suggestion: `Remove the unused import: import { ${name} } from '${info.source}'`,
157
- codeSnippet: getCodeSnippet(sourceCode, info.line),
158
- });
159
- }
160
- });
161
-
162
- return smells;
163
- }