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

feat(eslint-plugin): [strict-interface-implementation] add rule#11711

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

Draft
JoshuaKGoldberg wants to merge1 commit intotypescript-eslint:main
base:main
Choose a base branch
Loading
fromJoshuaKGoldberg:strict-interface-implementation
Draft
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
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
import type { TSESTree } from '@typescript-eslint/utils';
import type * as ts from 'typescript';

import { AST_NODE_TYPES } from '@typescript-eslint/utils';

import type { NodeWithKey } from '../util';

import {
createRule,
getParserServices,
getStaticMemberAccessValue,
isNodeWithKey,
} from '../util';

type NodeWithStaticKey = Exclude<
NodeWithKey,
| TSESTree.MemberExpressionComputedName
| TSESTree.MemberExpressionNonComputedName
>;

export default createRule({
name: 'strict-interface-implementation',
meta: {
type: 'problem',
docs: {
description:
'Enforce classes are fully assignable to any interfaces they implement',
requiresTypeChecking: true,
},
fixable: 'code',
messages: {
unassignable:
'This {{target}} is not fully assignable to the interface {{interface}} type for {{name}}.',
},
schema: [],
},
defaultOptions: [],
create(context) {
const services = getParserServices(context);
const checker = services.program.getTypeChecker();

function checkClassImplements(
node: TSESTree.ClassDeclaration | TSESTree.ClassExpression,
base: ts.Type,
) {
for (const element of node.body.body) {
if (element.type === AST_NODE_TYPES.MethodDefinition) {
checkMethod(element, base);
} else if (isNodeWithKey(element)) {
checkProperty(element, base);
}
}
}

function checkMethod(element: TSESTree.MethodDefinition, base: ts.Type) {
const methodName = getStaticMemberAccessValue(element, context);
if (typeof methodName !== 'string') {
return;
}

const baseMethod = base.getProperty(methodName);
if (!baseMethod?.valueDeclaration) {
return;
}

const baseType = checker.getTypeAtLocation(baseMethod.valueDeclaration);
const derivedType = services.getTypeAtLocation(element);

if (isMethodAssignable(baseType, derivedType)) {
return;
}

context.report({
node: element.key,
messageId: 'unassignable',
data: {
name: methodName,
interface: checker.typeToString(base),
target: 'method',
},
});
}

function isMethodAssignable(base: ts.Type, derived: ts.Type) {
const baseSignature = base.getCallSignatures()[0];
const derivedSignature = derived.getCallSignatures()[0];

if (
derivedSignature.parameters.length > baseSignature.parameters.length
) {
return false;
}

for (let i = 0; i < baseSignature.parameters.length; i += 1) {
const baseType = checker.getTypeOfSymbol(baseSignature.parameters[i]);
const derivedType = checker.getTypeOfSymbol(
derivedSignature.parameters[i],
);

if (!checker.isTypeAssignableTo(baseType, derivedType)) {
return false;
}
}

return true;
}

function checkProperty(element: NodeWithStaticKey, base: ts.Type) {
const propertyName = getStaticMemberAccessValue(element, context);
if (typeof propertyName !== 'string') {
return;
}

const baseProperty = base.getProperty(propertyName);
if (!baseProperty?.valueDeclaration) {
return;
}

const baseType = checker.getTypeAtLocation(baseProperty.valueDeclaration);
const derivedType = services.getTypeAtLocation(element);

if (checker.isTypeAssignableTo(baseType, derivedType)) {
return;
}

context.report({
node: element.key,
messageId: 'unassignable',
data: {
name: propertyName,
interface: checker.typeToString(base),
target: 'property',
},
});
}

function getSuperClassImplements(
superClass: TSESTree.LeftHandSideExpression,
) {
// TODO
}

return {
'ClassDeclaration, ClassExpression'(
node: TSESTree.ClassDeclaration | TSESTree.ClassExpression,
) {
for (const base of node.implements) {
checkClassImplements(node, services.getTypeAtLocation(base));
}

if (node.superClass) {
for (const base of getSuperClassImplements(node.superClass)) {
checkClassImplements(node, base);
}
}
},
};
},
});
15 changes: 15 additions & 0 deletionspackages/eslint-plugin/src/util/misc.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -248,6 +248,21 @@ export type NodeWithKey =
| TSESTree.TSAbstractMethodDefinition
| TSESTree.TSAbstractPropertyDefinition;

export function isNodeWithKey(node: TSESTree.Node): node is NodeWithKey {
switch (node.type) {
case AST_NODE_TYPES.AccessorProperty:
case AST_NODE_TYPES.MemberExpression:
case AST_NODE_TYPES.MethodDefinition:
case AST_NODE_TYPES.Property:
case AST_NODE_TYPES.PropertyDefinition:
case AST_NODE_TYPES.TSAbstractMethodDefinition:
case AST_NODE_TYPES.TSAbstractPropertyDefinition:
return true;
default:
return false;
}
}

/**
* Gets a member being accessed or declared if its value can be determined statically, and
* resolves it to the string or symbol value that will be used as the actual member
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
import { RuleTester } from '@typescript-eslint/rule-tester';

import rule from '../../src/rules/strict-interface-implementation';
import { getFixturesRootDir } from '../RuleTester';

const rootDir = getFixturesRootDir();
const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: rootDir,
},
},
});

ruleTester.run('strict-interface-implementation', rule, {
valid: [
'class Standalone {}',
'const Standalone = class {};',
'const Standalone = class Standalone {};',
`
interface Base {}
class Derived implements Base {}
`,
`
interface Base {
process(): void;
}
class Derived implements Base {
process() {}
}
`,
`
interface Base {
value: string;
}
class Derived implements Base {
value: string;
}
`,
],
invalid: [
{
code: `
interface Base {
value: string | undefined;
}

class Derived implements Base {
value: string;
}
`,
errors: [
{
data: {
interface: 'Base',
name: 'value',
target: 'property',
},
messageId: 'unassignable',
},
],
},
{
code: `
interface Base {
process(value: string | null): void;
}

class Derived implements Base {
public process(value: string) {}
}
`,
errors: [
{
data: {
interface: 'Base',
name: 'process',
target: 'method',
},
messageId: 'unassignable',
},
],
},
{
code: `
interface Base {
process(value?: string): void;
}

class Derived implements Base {
public process(value: string) {}
}
`,
errors: [
{
data: {
interface: 'Base',
name: 'process',
target: 'method',
},
messageId: 'unassignable',
},
],
},
],
});
Loading

[8]ページ先頭

©2009-2025 Movatter.jp