Skip to content
Open
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
75 changes: 75 additions & 0 deletions docs/rules/no-undefined-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,25 @@ let getValue = (callback) => {
callback(`hello`)
}
// Message: The type 'CustomElementConstructor' is undefined.

class MyClass {
method() {}
}
/** @type {MyClass.nonExistent} */
const a = 1;
// Message: The type 'MyClass.nonExistent' is undefined.

declare namespace MyNamespace {
type MyType = string;
interface FooBar {
foobiz: string;
}
}
/** @type {MyNamespace.MyType} */
const a = 's';
/** @type {MyNamespace.OtherType} */
const b = 's';
// Message: The type 'MyNamespace.OtherType' is undefined.
````


Expand Down Expand Up @@ -1125,5 +1144,61 @@ export default Severities;
const checkIsOnOf = (value, ...validValues) => {
return validValues.includes(value);
};

declare namespace MyNamespace {
type MyType = string;
interface FooBar {
foobiz: string;
}
}
/** @type {MyNamespace.MyType} */
const a = 's';
/** @type {MyNamespace.FooBar} */
const c = { foobiz: 's' };
/** @param {MyNamespace.FooBar['foobiz']} p */
function f(p) {}

declare namespace CoverageTest {
export class MyClass {}
export function myFunction();
const local = 1;
export { local };
}
/** @type {CoverageTest.MyClass} */
const x = null;

declare module "foo";

declare namespace MyNamespace {
interface I {
[key: string]: string;
(): void;
new (): void;
}
}

declare namespace Nested.Namespace {
class C {}
}

declare namespace NsWithInterface {
export interface HasIndexSig {
[key: string]: string;
normalProp: number;
}
}
/** @type {NsWithInterface.HasIndexSig} */
const x = { normalProp: 1 };
/** @type {NsWithInterface.HasIndexSig.normalProp} */
const y = 1;

declare namespace NsWithLiteralKey {
export interface HasLiteralKey {
"string-key": string;
normalProp: number;
}
}
/** @type {NsWithLiteralKey.HasLiteralKey} */
const x = { "string-key": "value", normalProp: 1 };
````

58 changes: 57 additions & 1 deletion src/rules/noUndefinedTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ export default iterateJsdoc(({
*/
const imports = [];

/** @type {Set<string>} */
const closedTypes = new Set();

const allDefinedTypes = new Set(globalScope.variables.map(({
name,
}) => {
Expand All @@ -296,6 +299,7 @@ export default iterateJsdoc(({
)?.parent;
switch (globalItem?.type) {
case 'ClassDeclaration':
closedTypes.add(name);
return [
name,
...globalItem.body.body.map((item) => {
Expand Down Expand Up @@ -330,6 +334,54 @@ export default iterateJsdoc(({
return `${name}.${property}`;
}).filter(Boolean),
];
case 'TSModuleDeclaration':
closedTypes.add(name);
return [
name,
/* c8 ignore next -- Guard for ambient modules without body. */
...(globalItem.body?.body || []).flatMap((item) => {
/** @type {import('@typescript-eslint/types').TSESTree.ProgramStatement | import('@typescript-eslint/types').TSESTree.NamedExportDeclarations | null} */
let declaration = item;

if (item.type === 'ExportNamedDeclaration' && item.declaration) {
declaration = item.declaration;
}

if (declaration.type === 'TSTypeAliasDeclaration' || declaration.type === 'ClassDeclaration') {
/* c8 ignore next 4 -- Guard for anonymous class declarations */
if (!declaration.id) {
return [];
}

return [
`${name}.${declaration.id.name}`,
];
}

if (declaration.type === 'TSInterfaceDeclaration') {
return [
`${name}.${declaration.id.name}`,
...declaration.body.body.map((prop) => {
// Only `TSPropertySignature` and `TSMethodSignature` have 'key'.
if (prop.type !== 'TSPropertySignature' && prop.type !== 'TSMethodSignature') {
return '';
}

// Key can be computed or a literal, only handle Identifier.
if (prop.key.type !== 'Identifier') {
return '';
}

const propName = prop.key.name;
/* c8 ignore next -- `propName` is always truthy for Identifiers */
return propName ? `${name}.${declaration.id.name}.${propName}` : '';
}).filter(Boolean),
];
}

return [];
}),
];
case 'VariableDeclarator':
if (/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
/** @type {import('@typescript-eslint/types').TSESTree.CallExpression} */ (
Expand Down Expand Up @@ -529,9 +581,13 @@ export default iterateJsdoc(({

if (type === 'JsdocTypeName') {
const structuredTypes = structuredTags[tag.tag]?.type;
const rootNamespace = val.split('.')[0];
const isNamespaceValid = (definedTypes.includes(rootNamespace) || allDefinedTypes.has(rootNamespace)) &&
!closedTypes.has(rootNamespace);

if (!allDefinedTypes.has(val) &&
!definedNamesAndNamepaths.has(val) &&
(!Array.isArray(structuredTypes) || !structuredTypes.includes(val))
(!Array.isArray(structuredTypes) || !structuredTypes.includes(val)) && !isNamespaceValid
) {
const parent =
/**
Expand Down
136 changes: 136 additions & 0 deletions test/rules/assertions/noUndefinedTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,44 @@ export default /** @type {import('../index.js').TestCases} */ ({
},
],
},
{
code: `
class MyClass {
method() {}
}
/** @type {MyClass.nonExistent} */
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we have the following examples based on class which pass in their access of a static property that does exist?

I.e., will your changes allow:

class MyClass {
}
MyClass.Existent = () => {};
/** @type {MyClass.Existent} */

and

class MyClass {
  static Existent = () => {};
}

/** @type {MyClass.Existent} */

const a = 1;
`,
errors: [
{
line: 5,
message: 'The type \'MyClass.nonExistent\' is undefined.',
},
],
},
{
code: `
declare namespace MyNamespace {
type MyType = string;
interface FooBar {
foobiz: string;
}
}
/** @type {MyNamespace.MyType} */
const a = 's';
/** @type {MyNamespace.OtherType} */
const b = 's';
`,
errors: [
{
line: 10,
message: 'The type \'MyNamespace.OtherType\' is undefined.',
},
],
languageOptions: {
parser: typescriptEslintParser,
},
},
],
valid: [
{
Expand Down Expand Up @@ -1904,5 +1942,103 @@ export default /** @type {import('../index.js').TestCases} */ ({
};
`,
},
{
code: `
declare namespace MyNamespace {
type MyType = string;
interface FooBar {
foobiz: string;
}
}
/** @type {MyNamespace.MyType} */
const a = 's';
/** @type {MyNamespace.FooBar} */
const c = { foobiz: 's' };
/** @param {MyNamespace.FooBar['foobiz']} p */
function f(p) {}
`,
languageOptions: {
parser: typescriptEslintParser,
},
},
{
code: `
declare namespace CoverageTest {
export class MyClass {}
export function myFunction();
const local = 1;
export { local };
}
/** @type {CoverageTest.MyClass} */
const x = null;
`,
languageOptions: {
parser: typescriptEslintParser,
},
},
{
code: `
declare module "foo";
`,
languageOptions: {
parser: typescriptEslintParser,
},
},
{
code: `
declare namespace MyNamespace {
interface I {
[key: string]: string;
(): void;
new (): void;
}
}
`,
languageOptions: {
parser: typescriptEslintParser,
},
},
{
code: `
declare namespace Nested.Namespace {
class C {}
}
`,
languageOptions: {
parser: typescriptEslintParser,
},
},
{
code: `
declare namespace NsWithInterface {
export interface HasIndexSig {
[key: string]: string;
normalProp: number;
}
}
/** @type {NsWithInterface.HasIndexSig} */
const x = { normalProp: 1 };
/** @type {NsWithInterface.HasIndexSig.normalProp} */
const y = 1;
`,
languageOptions: {
parser: typescriptEslintParser,
},
},
{
code: `
declare namespace NsWithLiteralKey {
export interface HasLiteralKey {
"string-key": string;
normalProp: number;
}
}
/** @type {NsWithLiteralKey.HasLiteralKey} */
const x = { "string-key": "value", normalProp: 1 };
`,
languageOptions: {
parser: typescriptEslintParser,
},
},
],
});
Loading