Skip to content
Merged
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
26 changes: 25 additions & 1 deletion eslint-factory/src/rules/require-new-url-try-catch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ const esmRuleTester = new RuleTester({
describe("require-new-url-try-catch", () => {
it("valid: new URL with string literal is always safe (CommonJS)", () => {
cjsRuleTester.run("require-new-url-try-catch", requireNewUrlTryCatchRule, {
valid: [`const u = new URL("https://github.com");`, `const u = new URL("https://github.com/owner/repo");`, `const u = new URL(\`https://github.com/static\`);`],
valid: [
`const u = new URL("https://github.com");`,
`const u = new URL("https://github.com/owner/repo");`,
`const u = new URL(\`https://github.com/static\`);`,
`const u = new URL("https://github.com" + "/owner/repo");`,
`const u = new URL("https://github.com" + \`/static\` + "/path");`,
],
invalid: [],
});
});
Expand Down Expand Up @@ -60,6 +66,7 @@ describe("require-new-url-try-catch", () => {
valid: [
// First arg is static, base is import.meta.url — never throws
`new URL("./relative/path", import.meta.url);`,
`const u = new URL(import.meta.url);`,
// First arg is dynamic but we're inside try
`try { new URL(path, import.meta.url); } catch (e) {}`,
],
Expand Down Expand Up @@ -137,6 +144,23 @@ describe("require-new-url-try-catch", () => {
});
});

it("invalid: new URL with string concatenation containing variables (CommonJS)", () => {
cjsRuleTester.run("require-new-url-try-catch", requireNewUrlTryCatchRule, {
valid: [],
invalid: [
{
code: `const u = new URL(host + "/x");`,
errors: [
{
messageId: "requireTryCatch",
data: { arg: 'host + "/x"' },
},
],
},
],
});
});

it("invalid: new URL reports in ES module", () => {
esmRuleTester.run("require-new-url-try-catch", requireNewUrlTryCatchRule, {
valid: [],
Expand Down
27 changes: 17 additions & 10 deletions eslint-factory/src/rules/require-new-url-try-catch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,21 +75,27 @@ export const requireNewUrlTryCatchRule = createRule({
return null;
}

/** Returns true when an expression is a compile-time constant string. */
function isStaticStringExpression(arg: TSESTree.CallExpressionArgument): boolean {
if (arg.type === AST_NODE_TYPES.Literal && typeof (arg as TSESTree.StringLiteral).value === "string") return true;
if (arg.type === AST_NODE_TYPES.TemplateLiteral && (arg as TSESTree.TemplateLiteral).expressions.length === 0) return true;
if (arg.type === AST_NODE_TYPES.BinaryExpression && arg.operator === "+") {
return isStaticStringExpression(arg.left) && isStaticStringExpression(arg.right);
}
return false;
}

/** Returns true when an argument is a runtime-dynamic expression (not a compile-time constant). */
function isDynamicArg(arg: TSESTree.CallExpressionArgument): boolean {
if (arg.type === "SpreadElement") return false;
// Literal strings are compile-time constants — no runtime parse risk.
if (arg.type === AST_NODE_TYPES.Literal && typeof (arg as TSESTree.StringLiteral).value === "string") return false;
// Template literals with no expressions are effectively string constants.
if (arg.type === AST_NODE_TYPES.TemplateLiteral && (arg as TSESTree.TemplateLiteral).expressions.length === 0) return false;
return true;
return !isStaticStringExpression(arg);
}

/**
* Returns true when a base argument is a known-safe compile-time value that never throws.
* Returns true when an argument is a known-safe value that never throws in URL position.
* `import.meta.url` is always a valid absolute URL in ES modules.
*/
function isKnownSafeBase(arg: TSESTree.CallExpressionArgument): boolean {
function isKnownSafeUrlArgument(arg: TSESTree.CallExpressionArgument): boolean {
// import.meta.url is a MemberExpression: { object: MetaProperty(import.meta), property: Identifier(url) }
if (arg.type !== AST_NODE_TYPES.MemberExpression) return false;
const memberExpr = arg as TSESTree.MemberExpression;
Expand All @@ -112,11 +118,12 @@ export const requireNewUrlTryCatchRule = createRule({

// `new URL()` with zero arguments always throws TypeError at runtime — always flag it.
const noArgs = firstArg === undefined;
// Flag when the first argument is a runtime-dynamic expression.
const firstArgDynamic = !noArgs && isDynamicArg(firstArg);
// Flag when the first argument is a runtime-dynamic expression, excluding known-safe
// values such as import.meta.url.
const firstArgDynamic = !noArgs && !isKnownSafeUrlArgument(firstArg) && isDynamicArg(firstArg);
// Flag when the second (base) argument is dynamic and not a known-safe value such as
// import.meta.url. An invalid base throws the same TypeError as an invalid URL string.
const secondArgDynamic = secondArg !== undefined && !isKnownSafeBase(secondArg) && isDynamicArg(secondArg);
const secondArgDynamic = secondArg !== undefined && !isKnownSafeUrlArgument(secondArg) && isDynamicArg(secondArg);

if (!noArgs && !firstArgDynamic && !secondArgDynamic) return;

Expand Down