Skip to content

Commit ca41183

Browse files
committed
fix(no-undefined-types): support strict validation for TS namespaces
- Adds `TSModuleDeclaration` to `closedTypes` to enforce strict validation of namespace members. - Recursively finds exported types (`TSTypeAliasDeclaration`, `TSInterfaceDeclaration`) within namespaces. - Adds support for interface properties within namespaces. - Updates tests to verify strict validation of namespace members.
1 parent f219b62 commit ca41183

3 files changed

Lines changed: 268 additions & 1 deletion

File tree

docs/rules/no-undefined-types.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,25 @@ let getValue = (callback) => {
407407
callback(`hello`)
408408
}
409409
// Message: The type 'CustomElementConstructor' is undefined.
410+
411+
class MyClass {
412+
method() {}
413+
}
414+
/** @type {MyClass.nonExistent} */
415+
const a = 1;
416+
// Message: The type 'MyClass.nonExistent' is undefined.
417+
418+
declare namespace MyNamespace {
419+
type MyType = string;
420+
interface FooBar {
421+
foobiz: string;
422+
}
423+
}
424+
/** @type {MyNamespace.MyType} */
425+
const a = 's';
426+
/** @type {MyNamespace.OtherType} */
427+
const b = 's';
428+
// Message: The type 'MyNamespace.OtherType' is undefined.
410429
````
411430

412431

@@ -1125,5 +1144,61 @@ export default Severities;
11251144
const checkIsOnOf = (value, ...validValues) => {
11261145
return validValues.includes(value);
11271146
};
1147+
1148+
declare namespace MyNamespace {
1149+
type MyType = string;
1150+
interface FooBar {
1151+
foobiz: string;
1152+
}
1153+
}
1154+
/** @type {MyNamespace.MyType} */
1155+
const a = 's';
1156+
/** @type {MyNamespace.FooBar} */
1157+
const c = { foobiz: 's' };
1158+
/** @param {MyNamespace.FooBar['foobiz']} p */
1159+
function f(p) {}
1160+
1161+
declare namespace CoverageTest {
1162+
export class MyClass {}
1163+
export function myFunction();
1164+
const local = 1;
1165+
export { local };
1166+
}
1167+
/** @type {CoverageTest.MyClass} */
1168+
const x = null;
1169+
1170+
declare module "foo";
1171+
1172+
declare namespace MyNamespace {
1173+
interface I {
1174+
[key: string]: string;
1175+
(): void;
1176+
new (): void;
1177+
}
1178+
}
1179+
1180+
declare namespace Nested.Namespace {
1181+
class C {}
1182+
}
1183+
1184+
declare namespace NsWithInterface {
1185+
export interface HasIndexSig {
1186+
[key: string]: string;
1187+
normalProp: number;
1188+
}
1189+
}
1190+
/** @type {NsWithInterface.HasIndexSig} */
1191+
const x = { normalProp: 1 };
1192+
/** @type {NsWithInterface.HasIndexSig.normalProp} */
1193+
const y = 1;
1194+
1195+
declare namespace NsWithLiteralKey {
1196+
export interface HasLiteralKey {
1197+
"string-key": string;
1198+
normalProp: number;
1199+
}
1200+
}
1201+
/** @type {NsWithLiteralKey.HasLiteralKey} */
1202+
const x = { "string-key": "value", normalProp: 1 };
11281203
````
11291204

src/rules/noUndefinedTypes.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,9 @@ export default iterateJsdoc(({
274274
*/
275275
const imports = [];
276276

277+
/** @type {Set<string>} */
278+
const closedTypes = new Set();
279+
277280
const allDefinedTypes = new Set(globalScope.variables.map(({
278281
name,
279282
}) => {
@@ -296,6 +299,7 @@ export default iterateJsdoc(({
296299
)?.parent;
297300
switch (globalItem?.type) {
298301
case 'ClassDeclaration':
302+
closedTypes.add(name);
299303
return [
300304
name,
301305
...globalItem.body.body.map((item) => {
@@ -330,6 +334,54 @@ export default iterateJsdoc(({
330334
return `${name}.${property}`;
331335
}).filter(Boolean),
332336
];
337+
case 'TSModuleDeclaration':
338+
closedTypes.add(name);
339+
return [
340+
name,
341+
/* c8 ignore next -- Guard for ambient modules without body. */
342+
...(globalItem.body?.body || []).flatMap((item) => {
343+
/** @type {import('@typescript-eslint/types').TSESTree.ProgramStatement | import('@typescript-eslint/types').TSESTree.NamedExportDeclarations | null} */
344+
let declaration = item;
345+
346+
if (item.type === 'ExportNamedDeclaration' && item.declaration) {
347+
declaration = item.declaration;
348+
}
349+
350+
if (declaration.type === 'TSTypeAliasDeclaration' || declaration.type === 'ClassDeclaration') {
351+
/* c8 ignore next 4 -- Guard for anonymous class declarations */
352+
if (!declaration.id) {
353+
return [];
354+
}
355+
356+
return [
357+
`${name}.${declaration.id.name}`,
358+
];
359+
}
360+
361+
if (declaration.type === 'TSInterfaceDeclaration') {
362+
return [
363+
`${name}.${declaration.id.name}`,
364+
...declaration.body.body.map((prop) => {
365+
// Only `TSPropertySignature` and `TSMethodSignature` have 'key'.
366+
if (prop.type !== 'TSPropertySignature' && prop.type !== 'TSMethodSignature') {
367+
return '';
368+
}
369+
370+
// Key can be computed or a literal, only handle Identifier.
371+
if (prop.key.type !== 'Identifier') {
372+
return '';
373+
}
374+
375+
const propName = prop.key.name;
376+
/* c8 ignore next -- `propName` is always truthy for Identifiers */
377+
return propName ? `${name}.${declaration.id.name}.${propName}` : '';
378+
}).filter(Boolean),
379+
];
380+
}
381+
382+
return [];
383+
}),
384+
];
333385
case 'VariableDeclarator':
334386
if (/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
335387
/** @type {import('@typescript-eslint/types').TSESTree.CallExpression} */ (
@@ -529,9 +581,13 @@ export default iterateJsdoc(({
529581

530582
if (type === 'JsdocTypeName') {
531583
const structuredTypes = structuredTags[tag.tag]?.type;
584+
const rootNamespace = val.split('.')[0];
585+
const isNamespaceValid = (definedTypes.includes(rootNamespace) || allDefinedTypes.has(rootNamespace)) &&
586+
!closedTypes.has(rootNamespace);
587+
532588
if (!allDefinedTypes.has(val) &&
533589
!definedNamesAndNamepaths.has(val) &&
534-
(!Array.isArray(structuredTypes) || !structuredTypes.includes(val))
590+
(!Array.isArray(structuredTypes) || !structuredTypes.includes(val)) && !isNamespaceValid
535591
) {
536592
const parent =
537593
/**

test/rules/assertions/noUndefinedTypes.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,44 @@ export default /** @type {import('../index.js').TestCases} */ ({
701701
},
702702
],
703703
},
704+
{
705+
code: `
706+
class MyClass {
707+
method() {}
708+
}
709+
/** @type {MyClass.nonExistent} */
710+
const a = 1;
711+
`,
712+
errors: [
713+
{
714+
line: 5,
715+
message: 'The type \'MyClass.nonExistent\' is undefined.',
716+
},
717+
],
718+
},
719+
{
720+
code: `
721+
declare namespace MyNamespace {
722+
type MyType = string;
723+
interface FooBar {
724+
foobiz: string;
725+
}
726+
}
727+
/** @type {MyNamespace.MyType} */
728+
const a = 's';
729+
/** @type {MyNamespace.OtherType} */
730+
const b = 's';
731+
`,
732+
errors: [
733+
{
734+
line: 10,
735+
message: 'The type \'MyNamespace.OtherType\' is undefined.',
736+
},
737+
],
738+
languageOptions: {
739+
parser: typescriptEslintParser,
740+
},
741+
},
704742
],
705743
valid: [
706744
{
@@ -1904,5 +1942,103 @@ export default /** @type {import('../index.js').TestCases} */ ({
19041942
};
19051943
`,
19061944
},
1945+
{
1946+
code: `
1947+
declare namespace MyNamespace {
1948+
type MyType = string;
1949+
interface FooBar {
1950+
foobiz: string;
1951+
}
1952+
}
1953+
/** @type {MyNamespace.MyType} */
1954+
const a = 's';
1955+
/** @type {MyNamespace.FooBar} */
1956+
const c = { foobiz: 's' };
1957+
/** @param {MyNamespace.FooBar['foobiz']} p */
1958+
function f(p) {}
1959+
`,
1960+
languageOptions: {
1961+
parser: typescriptEslintParser,
1962+
},
1963+
},
1964+
{
1965+
code: `
1966+
declare namespace CoverageTest {
1967+
export class MyClass {}
1968+
export function myFunction();
1969+
const local = 1;
1970+
export { local };
1971+
}
1972+
/** @type {CoverageTest.MyClass} */
1973+
const x = null;
1974+
`,
1975+
languageOptions: {
1976+
parser: typescriptEslintParser,
1977+
},
1978+
},
1979+
{
1980+
code: `
1981+
declare module "foo";
1982+
`,
1983+
languageOptions: {
1984+
parser: typescriptEslintParser,
1985+
},
1986+
},
1987+
{
1988+
code: `
1989+
declare namespace MyNamespace {
1990+
interface I {
1991+
[key: string]: string;
1992+
(): void;
1993+
new (): void;
1994+
}
1995+
}
1996+
`,
1997+
languageOptions: {
1998+
parser: typescriptEslintParser,
1999+
},
2000+
},
2001+
{
2002+
code: `
2003+
declare namespace Nested.Namespace {
2004+
class C {}
2005+
}
2006+
`,
2007+
languageOptions: {
2008+
parser: typescriptEslintParser,
2009+
},
2010+
},
2011+
{
2012+
code: `
2013+
declare namespace NsWithInterface {
2014+
export interface HasIndexSig {
2015+
[key: string]: string;
2016+
normalProp: number;
2017+
}
2018+
}
2019+
/** @type {NsWithInterface.HasIndexSig} */
2020+
const x = { normalProp: 1 };
2021+
/** @type {NsWithInterface.HasIndexSig.normalProp} */
2022+
const y = 1;
2023+
`,
2024+
languageOptions: {
2025+
parser: typescriptEslintParser,
2026+
},
2027+
},
2028+
{
2029+
code: `
2030+
declare namespace NsWithLiteralKey {
2031+
export interface HasLiteralKey {
2032+
"string-key": string;
2033+
normalProp: number;
2034+
}
2035+
}
2036+
/** @type {NsWithLiteralKey.HasLiteralKey} */
2037+
const x = { "string-key": "value", normalProp: 1 };
2038+
`,
2039+
languageOptions: {
2040+
parser: typescriptEslintParser,
2041+
},
2042+
},
19072043
],
19082044
});

0 commit comments

Comments
 (0)