From d83b85353a85142f8ebe49aa4f0197605d9c4813 Mon Sep 17 00:00:00 2001 From: Daniel Chen Date: Mon, 4 May 2026 16:24:39 +0000 Subject: [PATCH 1/4] feat: add no-deprecated-url-parse lint rule Rebased onto main to resolve conflicts after #51 merge. --- README.md | 13 ++--- src/rules/index.ts | 2 + src/rules/meta.ts | 1 + src/rules/no-deprecated-url-parse.ts | 73 +++++++++++++++++++++++++++ tests/config-modes.test.ts | 2 +- tests/no-deprecated-url-parse.test.ts | 73 +++++++++++++++++++++++++++ 6 files changed, 157 insertions(+), 7 deletions(-) create mode 100644 src/rules/no-deprecated-url-parse.ts create mode 100644 tests/no-deprecated-url-parse.test.ts diff --git a/README.md b/README.md index 2c32f34..ebb3cb8 100644 --- a/README.md +++ b/README.md @@ -231,12 +231,13 @@ const backendRules = getRulesForPlatform('backend'); ### General Rules -| Rule | Severity | Platform | Description | -| ------------------------ | -------- | --------- | -------------------------------------------------------------- | -| `prefer-lucide-icons` | warning | expo, web | Prefer lucide-react/lucide-react-native icons | -| `no-react-native-in-web` | error | web | Don't import react-native in web modules (causes ESM failures) | -| `prefer-lucide-icons` | warning | expo, web | Prefer lucide-react/lucide-react-native icons | -| `no-module-level-new` | error | web | Don't use `new` at module scope (crashes during SSR) | +| Rule | Severity | Platform | Description | +| ------------------------- | -------- | --------- | -------------------------------------------------------------- | +| `prefer-lucide-icons` | warning | expo, web | Prefer lucide-react/lucide-react-native icons | +| `no-react-native-in-web` | error | web | Don't import react-native in web modules (causes ESM failures) | +| `prefer-lucide-icons` | warning | expo, web | Prefer lucide-react/lucide-react-native icons | +| `no-module-level-new` | error | web | Don't use `new` at module scope (crashes during SSR) | +| `no-deprecated-url-parse` | warning | backend | Use `new URL()` instead of deprecated `url.parse()` | --- diff --git a/src/rules/index.ts b/src/rules/index.ts index bd58375..2c81c45 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -52,6 +52,7 @@ import { noServerImportInClient } from './no-server-import-in-client'; import { ssrBrowserApiGuard } from './ssr-browser-api-guard'; import { noReactNativeInWeb } from './no-react-native-in-web'; import { noModuleLevelNew } from './no-module-level-new'; +import { noDeprecatedUrlParse } from './no-deprecated-url-parse'; export const rules: Record = { 'no-relative-paths': noRelativePaths, @@ -107,4 +108,5 @@ export const rules: Record = { 'ssr-browser-api-guard': ssrBrowserApiGuard, 'no-react-native-in-web': noReactNativeInWeb, 'no-module-level-new': noModuleLevelNew, + 'no-deprecated-url-parse': noDeprecatedUrlParse, }; diff --git a/src/rules/meta.ts b/src/rules/meta.ts index 060d249..8fe1cf4 100644 --- a/src/rules/meta.ts +++ b/src/rules/meta.ts @@ -51,6 +51,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..9e2a3c6 --- /dev/null +++ b/src/rules/no-deprecated-url-parse.ts @@ -0,0 +1,73 @@ +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/config-modes.test.ts b/tests/config-modes.test.ts index c18404f..d8bbfed 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(53); + expect(ruleNames.length).toBe(54); }); }); }); 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 7e9999ab19a45405ee4f5fbdd003752bf3056b3d Mon Sep 17 00:00:00 2001 From: Daniel Chen Date: Mon, 4 May 2026 18:10:46 +0000 Subject: [PATCH 2/4] chore: prettier --write README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fafddd1..9db9817 100644 --- a/README.md +++ b/README.md @@ -241,11 +241,11 @@ const backendRules = getRulesForPlatform('backend'); | `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-unrestricted-loop-in-serverless` | error | backend | Unbounded loops (while(true), for(;;)) cause serverless timeouts | -| `prefer-lucide-icons` | warning | expo, web | Prefer lucide-react/lucide-react-native icons | -| `no-react-native-in-web` | error | web | Don't import react-native in web modules (causes ESM failures) | -| `prefer-lucide-icons` | warning | expo, web | Prefer lucide-react/lucide-react-native icons | -| `no-module-level-new` | error | web | Don't use `new` at module scope (crashes during SSR) | -| `no-deprecated-url-parse` | warning | backend | Use `new URL()` instead of deprecated `url.parse()` | +| `prefer-lucide-icons` | warning | expo, web | Prefer lucide-react/lucide-react-native icons | +| `no-react-native-in-web` | error | web | Don't import react-native in web modules (causes ESM failures) | +| `prefer-lucide-icons` | warning | expo, web | Prefer lucide-react/lucide-react-native icons | +| `no-module-level-new` | error | web | Don't use `new` at module scope (crashes during SSR) | +| `no-deprecated-url-parse` | warning | backend | Use `new URL()` instead of deprecated `url.parse()` | --- From 843e3a944cffdc2adac98e31705bd33d6c0b2f30 Mon Sep 17 00:00:00 2001 From: Daniel Chen Date: Sun, 10 May 2026 18:35:09 +0000 Subject: [PATCH 3/4] fix(no-deprecated-url-parse): exempt URL.parse static method from rule lainterr[bot] flagged that the rule incorrectly fires on URL.parse(...), which is the modern WHATWG URL static method (Node 22+, browsers) and a valid replacement for the deprecated url.parse(). Drop the objectName === 'URL' branch and document why. Add a regression test. --- src/rules/no-deprecated-url-parse.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/rules/no-deprecated-url-parse.ts b/src/rules/no-deprecated-url-parse.ts index 9e2a3c6..9d01d46 100644 --- a/src/rules/no-deprecated-url-parse.ts +++ b/src/rules/no-deprecated-url-parse.ts @@ -41,11 +41,10 @@ export function noDeprecatedUrlParse(ast: File, _code: string): LintResult[] { callee.property.name === 'parse' ) { const objectName = t.isIdentifier(callee.object) ? callee.object.name : null; - if ( - objectName === 'url' || - objectName === 'URL' || - urlImportedNames.has(objectName ?? '') - ) { + // Note: only the lowercase legacy `url` module is deprecated. `URL.parse` + // is the modern static method on the WHATWG URL constructor (Node 22+, + // browsers) and is a valid replacement — do not flag it. + if (objectName === 'url' || urlImportedNames.has(objectName ?? '')) { results.push({ rule: RULE_NAME, message: "url.parse() is deprecated. Use 'new URL(input, base)' instead", From 9b57524b99bc12087665bcc9a07a51e1fd4e87e4 Mon Sep 17 00:00:00 2001 From: Daniel Chen Date: Sun, 10 May 2026 18:35:17 +0000 Subject: [PATCH 4/4] test(no-deprecated-url-parse): assert URL.parse static method is not flagged --- tests/no-deprecated-url-parse.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/no-deprecated-url-parse.test.ts b/tests/no-deprecated-url-parse.test.ts index 228189a..4bae5aa 100644 --- a/tests/no-deprecated-url-parse.test.ts +++ b/tests/no-deprecated-url-parse.test.ts @@ -70,4 +70,12 @@ describe('no-deprecated-url-parse rule', () => { const results = lintJsxCode(code, config); expect(results).toHaveLength(0); }); + + it('should not flag URL.parse static method (modern WHATWG URL API)', () => { + const code = ` + const result = URL.parse('https://example.com'); + `; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(0); + }); });