Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

fix(eslint-plugin): [restrict-template-expressions] check base types in allow list#11764

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Show file tree
Hide file tree
Changes fromall 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
50 changes: 10 additions & 40 deletionspackages/eslint-plugin/src/rules/no-base-to-string.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ import {
getConstrainedTypeAtLocation,
getParserServices,
getTypeName,
matchesTypeOrBaseType,
nullThrows,
} from '../util';

Expand DownExpand Up@@ -77,7 +78,8 @@ export default createRule<Options, MessageIds>({
],
create(context, [option]) {
const services = getParserServices(context);
const checker = services.program.getTypeChecker();
const { program } = services;
const checker = program.getTypeChecker();
const ignoredTypeNames = option.ignoredTypeNames ?? [];

function checkExpression(node: TSESTree.Expression, type?: ts.Type): void {
Expand DownExpand Up@@ -211,44 +213,6 @@ export default createRule<Options, MessageIds>({
return Usefulness.Always;
}

function getBaseTypesForType(type: ts.Type): readonly ts.Type[] {
if (!tsutils.isObjectType(type)) {
return [];
}

const interfaceTarget = tsutils.isTypeReference(type)
? type.target
: type;

const interfaceType =
tsutils.isObjectFlagSet(
interfaceTarget,
ts.ObjectFlags.Interface | ts.ObjectFlags.Class,
) && (interfaceTarget as ts.InterfaceType);

if (!interfaceType) {
return [];
}

return checker.getBaseTypes(interfaceType);
}

function isIgnoredTypeOrBase(
type: ts.Type,
seen = new Set<ts.Type>(),
): boolean {
if (seen.has(type)) {
return false;
}

seen.add(type);

return (
ignoredTypeNames.includes(getTypeName(checker, type)) ||
getBaseTypesForType(type).some(base => isIgnoredTypeOrBase(base, seen))
);
}

function collectToStringCertainty(
type: ts.Type,
visited: Set<ts.Type>,
Expand DownExpand Up@@ -286,7 +250,13 @@ export default createRule<Options, MessageIds>({
return Usefulness.Always;
}

if (isIgnoredTypeOrBase(type)) {
if (
matchesTypeOrBaseType(
services,
type => ignoredTypeNames.includes(getTypeName(checker, type)),
type,
)
) {
return Usefulness.Always;
}

Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ import {
isTypeAnyType,
isTypeFlagSet,
isTypeNeverType,
matchesTypeOrBaseType,
} from '../util';

type OptionTester = (
Expand DownExpand Up@@ -165,7 +166,11 @@ export default createRule<Options, MessageId>({

return (
isTypeFlagSet(innerType, TypeFlags.StringLike) ||
typeMatchesSomeSpecifier(innerType, allow, program) ||
matchesTypeOrBaseType(
services,
type => typeMatchesSomeSpecifier(type, allow, program),
innerType,
) ||
enabledOptionTesters.some(({ tester }) =>
tester(innerType, checker, recursivelyCheckType),
)
Expand Down
68 changes: 68 additions & 0 deletionspackages/eslint-plugin/src/util/baseTypeUtils.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
import type { ParserServicesWithTypeInformation } from '@typescript-eslint/utils';
import type { InterfaceType, Type } from 'typescript';

import { isObjectFlagSet, isObjectType } from 'ts-api-utils';
import * as tsutils from 'ts-api-utils';
import * as ts from 'typescript';

function getBaseTypesForType(
checker: ts.TypeChecker,
type: ts.Type,
): readonly ts.Type[] {
if (!tsutils.isObjectType(type)) {
return [];
}

const interfaceTarget = tsutils.isTypeReference(type) ? type.target : type;

const interfaceType =
tsutils.isObjectFlagSet(
interfaceTarget,
ts.ObjectFlags.Interface | ts.ObjectFlags.Class,
) && (interfaceTarget as ts.InterfaceType);

if (!interfaceType) {
return [];
}

return checker.getBaseTypes(interfaceType);
}

export function hasBaseTypes(type: Type): type is InterfaceType {
return (
isObjectType(type) &&
isObjectFlagSet(type, ts.ObjectFlags.Interface | ts.ObjectFlags.Class)
);
}

/**
* Recursively checks if a type or any of its base types matches the provided
* matcher function.
* @param services Parser services with type information
* @param matcher Function to test if a type matches the desired criteria
* @param type The type to check
* @param seen Set of already visited types to prevent infinite recursion
* @returns `true` if the type or any of its base types match the matcher
*/
export function matchesTypeOrBaseType(
services: ParserServicesWithTypeInformation,
matcher: (type: Type) => boolean,
type: Type,
seen = new Set<Type>(),
): boolean {
if (seen.has(type)) {
return false;
}

seen.add(type);

if (matcher(type)) {
return true;
}

const checker = services.program.getTypeChecker();

return getBaseTypesForType(checker, type).some(base =>
matchesTypeOrBaseType(services, matcher, base, seen),
);
}
1 change: 1 addition & 0 deletionspackages/eslint-plugin/src/util/index.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { ESLintUtils } from '@typescript-eslint/utils';

export * from './astUtils';
export * from './baseTypeUtils';
export * from './collectUnusedVariables';
export * from './createRule';
export * from './getFixOrSuggest';
Expand Down
View file
Open in desktop

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

[Testing] Oh, sory, I just realized - this is missing test coverage for extending multiple classes.

  • A extends B, C whereB is listed
  • A extends B, C whereC is listed
  • A extends B, C +B extends D, E whereE is listed

etc.

Original file line numberDiff line numberDiff line change
Expand Up@@ -344,6 +344,102 @@ ruleTester.run('restrict-template-expressions', rule, {
'const msg = `arg = ${undefined}`;',
'const msg = `arg = ${123}`;',
"const msg = `arg = ${'abc'}`;",
{
code: `
class Base {}
class Derived extends Base {}
const foo = new Base();
const bar = new Derived();
\`\${foo}\${bar}\`;
`,
options: [{ allow: [{ from: 'file', name: 'Base' }] }],
},
{
code: `
class Base {}
class Derived extends Base {}
class DerivedTwice extends Derived {}
const value = new DerivedTwice();
\`\${value}\`;
`,
options: [{ allow: [{ from: 'file', name: 'Base' }] }],
},
{
code: `
interface Base {
value: string;
}
interface Derived extends Base {
extra: number;
}
declare const obj: Derived;
\`\${obj}\`;
`,
options: [{ allow: [{ from: 'file', name: 'Base' }] }],
},
{
code: `
interface Base {
value: string;
}
interface Other {
other: number;
}
interface Derived extends Base, Other {
extra: boolean;
}
declare const obj: Derived;
\`\${obj}\`;
`,
options: [{ allow: [{ from: 'file', name: 'Base' }] }],
},
{
code: `
interface Base {
value: string;
}
interface Other {
other: number;
}
interface Derived extends Base, Other {
extra: boolean;
}
declare const obj: Derived;
\`\${obj}\`;
`,
options: [{ allow: [{ from: 'file', name: 'Other' }] }],
},
{
code: `
interface Root {
root: string;
}
interface Another {
another: string;
}
interface Base extends Root, Another {
value: string;
}
interface Other {
other: number;
}
interface Derived extends Base, Other {
extra: boolean;
}
declare const obj: Derived;
\`\${obj}\`;
`,
options: [{ allow: [{ from: 'file', name: 'Another' }] }],
},
// allow list with type alias without base types
{
code: `
type Custom = { value: string };
declare const obj: Custom;
\`\${obj}\`;
`,
options: [{ allow: [{ from: 'file', name: 'Custom' }] }],
},
],

invalid: [
Expand DownExpand Up@@ -614,5 +710,61 @@ ruleTester.run('restrict-template-expressions', rule, {
],
options: [{ allowAny: true }],
},
{
code: `
class Base {}
class Derived extends Base {}
const bar = new Derived();
\`\${bar}\`;
`,
errors: [
{
data: { type: 'Derived' },
messageId: 'invalidType',
},
],
options: [{ allow: [] }],
},
{
code: `
interface Base {
value: string;
}
interface Derived extends Base {
extra: number;
}
declare const obj: Derived;
\`\${obj}\`;
`,
errors: [
{
data: { type: 'Derived' },
messageId: 'invalidType',
},
],
options: [{ allow: [] }],
},
{
code: `
interface Base {
value: string;
}
interface Other {
other: number;
}
interface Derived extends Base, Other {
extra: boolean;
}
declare const obj: Derived;
\`\${obj}\`;
`,
errors: [
{
data: { type: 'Derived' },
messageId: 'invalidType',
},
],
options: [{ allow: [] }],
},
],
});

[8]ページ先頭

©2009-2025 Movatter.jp