Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const webRules = getRulesForPlatform('web');
const backendRules = getRulesForPlatform('backend');
```

## Available Rules (50 total)
## Available Rules (51 total)

### Expo Router Rules

Expand Down Expand Up @@ -192,6 +192,7 @@ const backendRules = getRulesForPlatform('backend');
| `no-response-json-lowercase` | warning | backend | Use Response.json() instead of new Response(JSON.stringify()) |
| `sql-no-nested-calls` | error | backend | Don't nest sql template tags |
| `no-sync-fs` | error | backend | Use fs.promises or fs/promises instead of sync fs methods |
| `no-deprecated-url-parse` | warning | backend | Use `new URL()` instead of deprecated `url.parse()` |

### URL Rules

Expand Down
2 changes: 2 additions & 0 deletions src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { noSyncFs } from './no-sync-fs';
import { preferNamedParams } from './prefer-named-params';
import { requireUseClient } from './require-use-client';
import { noServerImportInClient } from './no-server-import-in-client';
import { noDeprecatedUrlParse } from './no-deprecated-url-parse';

export const rules: Record<string, RuleFunction> = {
'no-relative-paths': noRelativePaths,
Expand Down Expand Up @@ -101,4 +102,5 @@ export const rules: Record<string, RuleFunction> = {
'prefer-named-params': preferNamedParams,
'require-use-client': requireUseClient,
'no-server-import-in-client': noServerImportInClient,
'no-deprecated-url-parse': noDeprecatedUrlParse,
};
1 change: 1 addition & 0 deletions src/rules/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const rulePlatforms: Partial<Record<string, Platform[]>> = {
'no-response-json-lowercase': ['backend'],
'sql-no-nested-calls': ['backend'],
'no-sync-fs': ['backend'],
'no-deprecated-url-parse': ['backend'],

// Universal rules (NOT listed here): prefer-guard-clauses, no-type-assertion,
// no-string-coerce-error
Expand Down
69 changes: 69 additions & 0 deletions src/rules/no-deprecated-url-parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import traverse from '@babel/traverse';
import * as t from '@babel/types';
import type { File } from '@babel/types';
import type { LintResult } from '../types';

const RULE_NAME = 'no-deprecated-url-parse';

export function noDeprecatedUrlParse(ast: File, _code: string): LintResult[] {
const results: LintResult[] = [];

// Track identifiers imported/required from 'url' or 'node:url'
const urlImportedNames = new Set<string>();

traverse(ast, {
ImportDeclaration(path) {
const source = path.node.source.value;
if (source !== 'url' && source !== 'node:url') return;

for (const specifier of path.node.specifiers) {
if (t.isImportSpecifier(specifier) && t.isIdentifier(specifier.local)) {
const imported = t.isIdentifier(specifier.imported)
? specifier.imported.name
: specifier.imported.value;
if (imported === 'parse') {
urlImportedNames.add(specifier.local.name);
}
}
if (t.isImportDefaultSpecifier(specifier) || t.isImportNamespaceSpecifier(specifier)) {
urlImportedNames.add(specifier.local.name);
}
}
},

CallExpression(path) {
const { callee, loc } = path.node;

// Pattern 1: url.parse(...)
if (
t.isMemberExpression(callee) &&
t.isIdentifier(callee.property) &&
callee.property.name === 'parse'
) {
const objectName = t.isIdentifier(callee.object) ? callee.object.name : null;
if (objectName === 'url' || objectName === 'URL' || urlImportedNames.has(objectName ?? '')) {
results.push({
rule: RULE_NAME,
message: "url.parse() is deprecated. Use 'new URL(input, base)' instead",
line: loc?.start.line ?? 0,
column: loc?.start.column ?? 0,
severity: 'warning',
});
}
}

// Pattern 2: parse(...) — direct named import from 'url'
if (t.isIdentifier(callee) && urlImportedNames.has(callee.name)) {
results.push({
rule: RULE_NAME,
message: "url.parse() is deprecated. Use 'new URL(input, base)' instead",
line: loc?.start.line ?? 0,
column: loc?.start.column ?? 0,
severity: 'warning',
});
}
},
});

return results;
}
2 changes: 1 addition & 1 deletion tests/config-modes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ describe('config modes', () => {
expect(ruleNames).toContain('no-relative-paths');
expect(ruleNames).toContain('expo-image-import');
expect(ruleNames).toContain('no-stylesheet-create');
expect(ruleNames.length).toBe(50);
expect(ruleNames.length).toBe(51);
});
});
});
73 changes: 73 additions & 0 deletions tests/no-deprecated-url-parse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, it, expect } from 'vitest';
import { lintJsxCode } from '../src';

const config = { rules: ['no-deprecated-url-parse'] };

describe('no-deprecated-url-parse rule', () => {
it('should detect url.parse() with default import', () => {
const code = `
import url from 'url';
const parsed = url.parse('https://example.com');
`;
const results = lintJsxCode(code, config);
expect(results).toHaveLength(1);
expect(results[0].rule).toBe('no-deprecated-url-parse');
expect(results[0].message).toContain('deprecated');
expect(results[0].message).toContain('new URL');
expect(results[0].severity).toBe('warning');
});

it('should detect url.parse() with namespace import', () => {
const code = `
import * as url from 'node:url';
const parsed = url.parse(req.url);
`;
const results = lintJsxCode(code, config);
expect(results).toHaveLength(1);
});

it('should detect named import of parse from url', () => {
const code = `
import { parse } from 'url';
const parsed = parse('https://example.com');
`;
const results = lintJsxCode(code, config);
expect(results).toHaveLength(1);
});

it('should detect aliased named import', () => {
const code = `
import { parse as urlParse } from 'url';
const parsed = urlParse('https://example.com');
`;
const results = lintJsxCode(code, config);
expect(results).toHaveLength(1);
});

it('should allow new URL()', () => {
const code = `
const parsed = new URL('https://example.com');
const withBase = new URL('/path', 'https://example.com');
`;
const results = lintJsxCode(code, config);
expect(results).toHaveLength(0);
});

it('should allow url.format() and other url methods', () => {
const code = `
import url from 'url';
const formatted = url.format({ protocol: 'https', hostname: 'example.com' });
`;
const results = lintJsxCode(code, config);
expect(results).toHaveLength(0);
});

it('should not flag parse() from non-url modules', () => {
const code = `
import { parse } from 'path';
const result = parse('/foo/bar.txt');
`;
const results = lintJsxCode(code, config);
expect(results).toHaveLength(0);
});
});
Loading