From 6519b819144497edab881c46f3097f5e781ad3d4 Mon Sep 17 00:00:00 2001 From: Daniel C Date: Mon, 2 Mar 2026 13:52:20 -0800 Subject: [PATCH 1/3] feat: add no-deprecated-url-parse lint rule Detects usage of url.parse() which is deprecated in Node.js. Suggests using 'new URL(input, base)' instead. Handles default imports, namespace imports, and named imports from 'url' and 'node:url'. 619+ deprecation warnings/week. --- src/rules/index.ts | 2 + src/rules/meta.ts | 1 + src/rules/no-deprecated-url-parse.ts | 69 +++++++++++++++++++++++++ tests/no-deprecated-url-parse.test.ts | 73 +++++++++++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 src/rules/no-deprecated-url-parse.ts create mode 100644 tests/no-deprecated-url-parse.test.ts diff --git a/src/rules/index.ts b/src/rules/index.ts index dd01b2a..e86546d 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -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 = { 'no-relative-paths': noRelativePaths, @@ -101,4 +102,5 @@ export const rules: Record = { 'prefer-named-params': preferNamedParams, 'require-use-client': requireUseClient, 'no-server-import-in-client': noServerImportInClient, + 'no-deprecated-url-parse': noDeprecatedUrlParse, }; diff --git a/src/rules/meta.ts b/src/rules/meta.ts index ef0a579..30178f0 100644 --- a/src/rules/meta.ts +++ b/src/rules/meta.ts @@ -48,6 +48,7 @@ export const rulePlatforms: Partial> = { '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 diff --git a/src/rules/no-deprecated-url-parse.ts b/src/rules/no-deprecated-url-parse.ts new file mode 100644 index 0000000..ba81aa2 --- /dev/null +++ b/src/rules/no-deprecated-url-parse.ts @@ -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(); + + 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; +} diff --git a/tests/no-deprecated-url-parse.test.ts b/tests/no-deprecated-url-parse.test.ts new file mode 100644 index 0000000..228189a --- /dev/null +++ b/tests/no-deprecated-url-parse.test.ts @@ -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); + }); +}); From 1d465b34b33c3c5ffd8828027ff8d61ef2e3ac55 Mon Sep 17 00:00:00 2001 From: Daniel C Date: Tue, 3 Mar 2026 10:40:06 -0800 Subject: [PATCH 2/3] fix: update README docs and test count for new rule --- README.md | 3 ++- tests/config-modes.test.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 89390df..8fd091f 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ const webRules = getRulesForPlatform('web'); const backendRules = getRulesForPlatform('backend'); ``` -## Available Rules (50 total) +## Available Rules (51 total) ### Expo Router Rules @@ -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 diff --git a/tests/config-modes.test.ts b/tests/config-modes.test.ts index 112bd5e..81092d3 100644 --- a/tests/config-modes.test.ts +++ b/tests/config-modes.test.ts @@ -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); }); }); }); From d09e329d7b91d390a74c5d769f620817a65d0f5a Mon Sep 17 00:00:00 2001 From: Daniel C Date: Tue, 3 Mar 2026 17:02:51 -0800 Subject: [PATCH 3/3] style: format README with prettier --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8fd091f..74fad27 100644 --- a/README.md +++ b/README.md @@ -192,7 +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()` | +| `no-deprecated-url-parse` | warning | backend | Use `new URL()` instead of deprecated `url.parse()` | ### URL Rules