chrome-devtools-frontend
Version:
Chrome DevTools UI
110 lines (96 loc) • 4.04 kB
text/typescript
// Copyright 2025 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Prefer `urlString` helper over sprinkling casts to
* `UrlString` across the codebase..
*/
import type {TSESLint, TSESTree} from '@typescript-eslint/utils';
import {createRule} from './utils/ruleCreator.ts';
// Define specific node types for clarity
type ImportDeclaration = TSESTree.ImportDeclaration;
type TSAsExpression = TSESTree.TSAsExpression;
type TSTypeReference = TSESTree.TSTypeReference;
type RuleFix = TSESLint.RuleFix;
export default createRule({
name: 'prefer-url-string',
meta: {
type: 'suggestion',
docs: {
description: 'Prefer `urlString` helper over manual casts to `UrlString` branded type.',
category: 'Best Practices',
},
messages: {
useUrlString: 'Use `urlString` helper instead of explicitly casting strings to the `UrlString` branded type',
},
fixable: 'code',
schema: [], // no options
},
defaultOptions: [],
create: function(context) {
const sourceCode = context.sourceCode;
let lastImportNode: ImportDeclaration|null = null;
let importTypeStarAsPlatformNode: ImportDeclaration|null = null;
function isImportTypeStarAsPlatform(node: ImportDeclaration): boolean {
return node.importKind === 'type' && node.specifiers.length === 1 &&
node.specifiers[0].type === 'ImportNamespaceSpecifier' && node.specifiers[0].local.type === 'Identifier' &&
node.specifiers[0].local.name === 'Platform';
}
function isPlatformDevToolsPathUrlString(node: TSESTree.TypeNode): node is TSTypeReference {
// Check if it's a TSTypeReference with a TSQualifiedName
if (node.type !== 'TSTypeReference' || node.typeName.type !== 'TSQualifiedName') {
return false;
}
const typeName = node.typeName;
// Check Platform.DevToolsPath part
if (typeName.left.type !== 'TSQualifiedName' || typeName.left.left.type !== 'Identifier' ||
typeName.left.left.name !== 'Platform' || typeName.left.right.type !== 'Identifier' ||
typeName.left.right.name !== 'DevToolsPath') {
return false;
}
// Check UrlString part
return typeName.right.type === 'Identifier' && typeName.right.name === 'UrlString';
}
function reportError(node: TSAsExpression) {
context.report({
node,
messageId: 'useUrlString',
fix(fixer) {
const urlText = node.expression.type === 'Literal' ?
String(node.expression.value).replace('$', '\\$').replace('`', '\\`') :
`\${${sourceCode.getText(node.expression)}}`;
const nodeText = `urlString\`${urlText}\``;
const fixers: RuleFix[] = [fixer.replaceText(node, nodeText)];
// Add import fix if needed
if (lastImportNode !== null) {
fixers.push(fixer.insertTextAfter(lastImportNode, '\n\nconst {urlString} = Platform.DevToolsPath;'));
// Prevent adding the import fix multiple times within the same file run
lastImportNode = null;
}
// Modify type import if needed
if (importTypeStarAsPlatformNode) {
const importStarAsPlatformText =
sourceCode.getText(importTypeStarAsPlatformNode).replace(/import\s+type/, 'import');
fixers.push(fixer.replaceText(importTypeStarAsPlatformNode, importStarAsPlatformText));
// Prevent modifying the import multiple times
importTypeStarAsPlatformNode = null;
}
return fixers;
},
});
}
return {
ImportDeclaration(node) {
lastImportNode = node;
if (isImportTypeStarAsPlatform(node)) {
importTypeStarAsPlatformNode = node;
}
},
TSAsExpression(node) {
if (isPlatformDevToolsPathUrlString(node.typeAnnotation)) {
reportError(node);
}
},
};
},
});