diff --git a/packages/kbn-docs-utils/scripts/update_fixture_comments.js b/packages/kbn-docs-utils/scripts/update_fixture_comments.js index 03d259762b052..fefcd5bcb707b 100644 --- a/packages/kbn-docs-utils/scripts/update_fixture_comments.js +++ b/packages/kbn-docs-utils/scripts/update_fixture_comments.js @@ -16,6 +16,7 @@ const categories = [ { key: 'missingComplexTypeInfo', title: 'missing complex type info' }, { key: 'isAnyType', title: 'any usage' }, { key: 'noReferences', title: 'no references' }, + { key: 'unnamedExports', title: 'unnamed exports' }, ]; /** @@ -64,11 +65,14 @@ const formatCategory = (title, entries) => { const sorted = [...entries].sort((a, b) => { const lineA = a.lineNumber ?? Number.MAX_SAFE_INTEGER; const lineB = b.lineNumber ?? Number.MAX_SAFE_INTEGER; - return lineA === lineB ? a.label.localeCompare(b.label) : lineA - lineB; + const labelA = a.label || a.textSnippet || ''; + const labelB = b.label || b.textSnippet || ''; + return lineA === lineB ? labelA.localeCompare(labelB) : lineA - lineB; }); sorted.forEach((entry) => { const lineInfo = entry.lineNumber != null ? `line ${entry.lineNumber}` : 'unknown line'; - lines.push(`// ${lineInfo} - ${entry.label}`); + const label = entry.label || entry.textSnippet || '(unnamed)'; + lines.push(`// ${lineInfo} - ${label}`); }); return lines.join('\n'); }; diff --git a/packages/kbn-docs-utils/src/__test_helpers__/mocks.ts b/packages/kbn-docs-utils/src/__test_helpers__/mocks.ts index eb70ee3430836..5d34f474ebf00 100644 --- a/packages/kbn-docs-utils/src/__test_helpers__/mocks.ts +++ b/packages/kbn-docs-utils/src/__test_helpers__/mocks.ts @@ -73,6 +73,7 @@ export const createMockPluginStats = (overrides: Partial = {}): ApiSta adoptionTrackedAPIs: [], adoptionTrackedAPIsCount: 0, adoptionTrackedAPIsUnreferencedCount: 0, + unnamedExports: [], ...overrides, }); @@ -108,5 +109,6 @@ export const createMockPluginMetaInfo = ( owner: { name: 'Test Team', githubTeam: 'test-team' }, description: 'A test plugin', isPlugin: true, + unnamedExports: [], ...overrides, }); diff --git a/packages/kbn-docs-utils/src/build_api_declarations/build_api_declaration.test.ts b/packages/kbn-docs-utils/src/build_api_declarations/build_api_declaration.test.ts index 8bf6dc0de0876..87baaafe7e1cc 100644 --- a/packages/kbn-docs-utils/src/build_api_declarations/build_api_declaration.test.ts +++ b/packages/kbn-docs-utils/src/build_api_declarations/build_api_declaration.test.ts @@ -48,7 +48,13 @@ beforeAll(() => { plugins = [getKibanaPlatformPlugin('pluginA')]; - nodes = getDeclarationNodesForPluginScope(project, plugins[0], ApiScope.CLIENT, log); + const { nodes: decNodes } = getDeclarationNodesForPluginScope( + project, + plugins[0], + ApiScope.CLIENT, + log + ); + nodes = decNodes; }); it('Test number primitive doc def', () => { diff --git a/packages/kbn-docs-utils/src/build_api_declarations/js_doc_utils.test.ts b/packages/kbn-docs-utils/src/build_api_declarations/js_doc_utils.test.ts index 30afada75bf0a..d60913ad48e49 100644 --- a/packages/kbn-docs-utils/src/build_api_declarations/js_doc_utils.test.ts +++ b/packages/kbn-docs-utils/src/build_api_declarations/js_doc_utils.test.ts @@ -152,6 +152,29 @@ describe('getJSDocParamComment', () => { const commentUpper = getJSDocParamComment(node!, 'A'); expect(commentUpper.length).toBe(0); // Case-sensitive, so 'A' won't match 'a' }); + + it('handles malformed @param with type but no name', () => { + const testProject = new Project({ + useInMemoryFileSystem: true, + }); + + const testSourceFile = testProject.createSourceFile( + 'test.ts', + ` + /** + * @param {Object} + */ + function malformedParam(obj: object) {} + ` + ); + + const func = testSourceFile.getFunction('malformedParam'); + expect(func).toBeDefined(); + + // Should not throw, and should return empty array since param name is missing in JSDoc. + const comment = getJSDocParamComment(func!, 'obj'); + expect(comment).toEqual([]); + }); }); describe('getJSDocReturnTagComment', () => { diff --git a/packages/kbn-docs-utils/src/check_package_docs_cli.test.ts b/packages/kbn-docs-utils/src/check_package_docs_cli.test.ts index d5f83fc2c8a9d..5421e66707d70 100644 --- a/packages/kbn-docs-utils/src/check_package_docs_cli.test.ts +++ b/packages/kbn-docs-utils/src/check_package_docs_cli.test.ts @@ -57,6 +57,7 @@ const createBaseStats = (pluginId: string): AllPluginStats => ({ eslintDisableFileCount: 0, eslintDisableLineCount: 0, enzymeImportCount: 0, + unnamedExports: [], }, }); diff --git a/packages/kbn-docs-utils/src/check_package_docs_cli.ts b/packages/kbn-docs-utils/src/check_package_docs_cli.ts index 16172b4a218af..6f239a566eb5c 100644 --- a/packages/kbn-docs-utils/src/check_package_docs_cli.ts +++ b/packages/kbn-docs-utils/src/check_package_docs_cli.ts @@ -28,9 +28,9 @@ import { import type { PluginOrPackage, MissingApiItemMap } from './types'; import type { AllPluginStats } from './cli/types'; -type ValidationCheck = 'any' | 'comments' | 'exports'; +type ValidationCheck = 'any' | 'comments' | 'exports' | 'unnamed'; -const DEFAULT_VALIDATION_CHECKS: ValidationCheck[] = ['any', 'comments', 'exports']; +const DEFAULT_VALIDATION_CHECKS: ValidationCheck[] = ['any', 'comments', 'exports', 'unnamed']; const rootDir = Path.join(__dirname, '../../..'); @@ -62,6 +62,7 @@ export const getValidationResults = ( const shouldCheckAny = checks.includes('any'); const shouldCheckComments = checks.includes('comments'); const shouldCheckExports = checks.includes('exports'); + const shouldCheckUnnamed = checks.includes('unnamed'); const hasPluginFilter = pluginFilter && pluginFilter.length > 0; const hasPackageFilter = packageFilter && packageFilter.length > 0; @@ -89,10 +90,11 @@ export const getValidationResults = ( pluginStats.paramDocMismatches.length > 0 || pluginStats.missingComplexTypeInfo.length > 0); const hasExportIssues = shouldCheckExports && missingExports > 0; + const hasUnnamedIssues = shouldCheckUnnamed && pluginStats.unnamedExports.length > 0; return { pluginId: plugin.id, - passed: !(hasAnyIssues || hasCommentIssues || hasExportIssues), + passed: !(hasAnyIssues || hasCommentIssues || hasExportIssues || hasUnnamedIssues), }; }); }; diff --git a/packages/kbn-docs-utils/src/cli/parse_cli_flags.test.ts b/packages/kbn-docs-utils/src/cli/parse_cli_flags.test.ts index 71336e9ca1e8a..f1bb0b6dc5c0b 100644 --- a/packages/kbn-docs-utils/src/cli/parse_cli_flags.test.ts +++ b/packages/kbn-docs-utils/src/cli/parse_cli_flags.test.ts @@ -85,7 +85,7 @@ describe('parseCliFlags', () => { const result = parseCliFlags(flags); - expect(result.stats).toEqual(['comments', 'any', 'exports']); + expect(result.stats).toEqual(['comments', 'any', 'exports', 'unnamed']); }); it('dedupes overlapping stats and check flags', () => { @@ -113,7 +113,7 @@ describe('parseCliFlags', () => { }; expect(() => parseCliFlags(flags)).toThrow( - 'expected --stats must only contain `any`, `comments` and/or `exports`' + 'expected --stats must only contain `any`, `comments`, `exports`, and/or `unnamed`' ); }); @@ -123,18 +123,18 @@ describe('parseCliFlags', () => { }; expect(() => parseCliFlags(flags)).toThrow( - 'expected --stats must only contain `any`, `comments` and/or `exports`' + 'expected --stats must only contain `any`, `comments`, `exports`, and/or `unnamed`' ); }); it('accepts valid stats values', () => { const flags: CliFlags = { - stats: ['any', 'comments', 'exports'], + stats: ['any', 'comments', 'exports', 'unnamed'], }; const result = parseCliFlags(flags); - expect(result.stats).toEqual(['any', 'comments', 'exports']); + expect(result.stats).toEqual(['any', 'comments', 'exports', 'unnamed']); }); it('handles references flag correctly', () => { diff --git a/packages/kbn-docs-utils/src/cli/parse_cli_flags.ts b/packages/kbn-docs-utils/src/cli/parse_cli_flags.ts index 771113736fd42..580a7671b9f8a 100644 --- a/packages/kbn-docs-utils/src/cli/parse_cli_flags.ts +++ b/packages/kbn-docs-utils/src/cli/parse_cli_flags.ts @@ -33,7 +33,7 @@ const normalizeStringList = (value: unknown | string[], flagName: string) => { const dedupe = (values: string[] | undefined) => values && values.length > 0 ? Array.from(new Set(values)) : undefined; -const VALID_CHECKS = ['any', 'comments', 'exports', 'all'] as const; +const VALID_CHECKS = ['any', 'comments', 'exports', 'unnamed', 'all'] as const; const normalizeCheckFlagValues = (check: unknown | string[]) => { if (!check) { @@ -46,7 +46,7 @@ const normalizeCheckFlagValues = (check: unknown | string[]) => { if (!isStringArray(check)) { throw createFlagError( - 'expected --check must only contain `any`, `comments`, `exports`, or `all`' + 'expected --check must only contain `any`, `comments`, `exports`, `unnamed`, or `all`' ); } @@ -58,12 +58,14 @@ const expandChecks = (checks: string[] | undefined) => { return undefined; } - const expanded = checks.flatMap((c) => (c === 'all' ? ['any', 'comments', 'exports'] : [c])); + const expanded = checks.flatMap((c) => + c === 'all' ? ['any', 'comments', 'exports', 'unnamed'] : [c] + ); const invalid = expanded.find((c) => !VALID_CHECKS.includes(c as (typeof VALID_CHECKS)[number])); if (invalid) { throw createFlagError( - 'expected --check must only contain `any`, `comments`, `exports`, or `all`' + 'expected --check must only contain `any`, `comments`, `exports`, `unnamed`, or `all`' ); } @@ -71,8 +73,11 @@ const expandChecks = (checks: string[] | undefined) => { }; const validateStats = (stats: string[] | undefined) => { - if (stats && stats.find((s) => s !== 'any' && s !== 'comments' && s !== 'exports')) { - throw createFlagError('expected --stats must only contain `any`, `comments` and/or `exports`'); + const validValues = ['any', 'comments', 'exports', 'unnamed']; + if (stats && stats.find((s) => !validValues.includes(s))) { + throw createFlagError( + 'expected --stats must only contain `any`, `comments`, `exports`, and/or `unnamed`' + ); } }; @@ -86,7 +91,9 @@ const normalizeStats = (value: unknown | string[]) => { } if (!isStringArray(value)) { - throw createFlagError('expected --stats must only contain `any`, `comments` and/or `exports`'); + throw createFlagError( + 'expected --stats must only contain `any`, `comments`, `exports`, and/or `unnamed`' + ); } return value; diff --git a/packages/kbn-docs-utils/src/cli/tasks/build_api_map.ts b/packages/kbn-docs-utils/src/cli/tasks/build_api_map.ts index 8fa5444ba1533..50ea9ffc55f7a 100644 --- a/packages/kbn-docs-utils/src/cli/tasks/build_api_map.ts +++ b/packages/kbn-docs-utils/src/cli/tasks/build_api_map.ts @@ -45,6 +45,7 @@ export function buildApiMap( unreferencedDeprecations, referencedDeprecations, adoptionTrackedAPIs, + unnamedExports, } = getPluginApiMap(project, plugins, log, { collectReferences: options.collectReferences, pluginFilter: options.pluginFilter, @@ -58,5 +59,6 @@ export function buildApiMap( referencedDeprecations, unreferencedDeprecations, adoptionTrackedAPIs, + unnamedExports, }; } diff --git a/packages/kbn-docs-utils/src/cli/tasks/collect_stats.test.ts b/packages/kbn-docs-utils/src/cli/tasks/collect_stats.test.ts index e2446555430cd..8511baba6dce9 100644 --- a/packages/kbn-docs-utils/src/cli/tasks/collect_stats.test.ts +++ b/packages/kbn-docs-utils/src/cli/tasks/collect_stats.test.ts @@ -70,6 +70,7 @@ describe('collectStats', () => { referencedDeprecations: {}, unreferencedDeprecations: {}, adoptionTrackedAPIs: {}, + unnamedExports: {}, }; (collectApiStatsForPlugin as jest.Mock).mockReturnValue({ @@ -85,6 +86,7 @@ describe('collectStats', () => { adoptionTrackedAPIs: [], adoptionTrackedAPIsCount: 0, adoptionTrackedAPIsUnreferencedCount: 0, + unnamedExports: [], }); (countEslintDisableLines as jest.Mock).mockResolvedValue({ diff --git a/packages/kbn-docs-utils/src/cli/tasks/collect_stats.ts b/packages/kbn-docs-utils/src/cli/tasks/collect_stats.ts index d42ebb80461cf..497c609ecf7ef 100644 --- a/packages/kbn-docs-utils/src/cli/tasks/collect_stats.ts +++ b/packages/kbn-docs-utils/src/cli/tasks/collect_stats.ts @@ -38,8 +38,13 @@ export async function collectStats( options: CliOptions ): Promise { const { plugins, pathsByPlugin } = setupResult; - const { pluginApiMap, missingApiItems, referencedDeprecations, adoptionTrackedAPIs } = - apiMapResult; + const { + pluginApiMap, + missingApiItems, + referencedDeprecations, + adoptionTrackedAPIs, + unnamedExports, + } = apiMapResult; const allPluginStats: AllPluginStats = {}; @@ -65,6 +70,7 @@ export async function collectStats( missingApiItems, referencedDeprecations, adoptionTrackedAPIs, + unnamedExports, }), owner: plugin.manifest.owner, description: plugin.manifest.description, diff --git a/packages/kbn-docs-utils/src/cli/tasks/report_metrics.test.ts b/packages/kbn-docs-utils/src/cli/tasks/report_metrics.test.ts index b927a026a8ab8..9d078a60c6093 100644 --- a/packages/kbn-docs-utils/src/cli/tasks/report_metrics.test.ts +++ b/packages/kbn-docs-utils/src/cli/tasks/report_metrics.test.ts @@ -72,6 +72,7 @@ describe('reportMetrics', () => { referencedDeprecations: {}, unreferencedDeprecations: {}, adoptionTrackedAPIs: {}, + unnamedExports: {}, }; allPluginStats = { @@ -94,6 +95,7 @@ describe('reportMetrics', () => { eslintDisableLineCount: 0, eslintDisableFileCount: 0, enzymeImportCount: 0, + unnamedExports: [], }, }; }); diff --git a/packages/kbn-docs-utils/src/cli/tasks/write_docs.test.ts b/packages/kbn-docs-utils/src/cli/tasks/write_docs.test.ts index 2465777aabdb9..dcbfa4e914285 100644 --- a/packages/kbn-docs-utils/src/cli/tasks/write_docs.test.ts +++ b/packages/kbn-docs-utils/src/cli/tasks/write_docs.test.ts @@ -84,6 +84,7 @@ describe('writeDocs', () => { referencedDeprecations: {}, unreferencedDeprecations: {}, adoptionTrackedAPIs: {}, + unnamedExports: {}, }; allPluginStats = { @@ -106,6 +107,7 @@ describe('writeDocs', () => { eslintDisableLineCount: 0, eslintDisableFileCount: 0, enzymeImportCount: 0, + unnamedExports: [], }, }; diff --git a/packages/kbn-docs-utils/src/cli/types.ts b/packages/kbn-docs-utils/src/cli/types.ts index 2e0f01c32f6fa..b2e3fa26b8ae4 100644 --- a/packages/kbn-docs-utils/src/cli/types.ts +++ b/packages/kbn-docs-utils/src/cli/types.ts @@ -17,6 +17,7 @@ import type { ReferencedDeprecationsByPlugin, UnreferencedDeprecationsByPlugin, AdoptionTrackedAPIsByPlugin, + UnnamedExportsByPlugin, ApiStats, PluginMetaInfo, } from '../types'; @@ -95,6 +96,8 @@ export interface BuildApiMapResult { unreferencedDeprecations: UnreferencedDeprecationsByPlugin; /** Adoption-tracked APIs. */ adoptionTrackedAPIs: AdoptionTrackedAPIsByPlugin; + /** Unnamed exports found during API collection. */ + unnamedExports: UnnamedExportsByPlugin; } /** diff --git a/packages/kbn-docs-utils/src/get_declaration_nodes_for_plugin.test.ts b/packages/kbn-docs-utils/src/get_declaration_nodes_for_plugin.test.ts index 44564608ba89f..382ef0407abb9 100644 --- a/packages/kbn-docs-utils/src/get_declaration_nodes_for_plugin.test.ts +++ b/packages/kbn-docs-utils/src/get_declaration_nodes_for_plugin.test.ts @@ -38,9 +38,9 @@ describe('getDeclarationNodesForPluginScope', () => { }, }; - const nodes = getDeclarationNodesForPluginScope(project, plugin, ApiScope.CLIENT, log); + const result = getDeclarationNodesForPluginScope(project, plugin, ApiScope.CLIENT, log); - expect(nodes).toEqual([]); + expect(result).toEqual({ nodes: [], unnamedExports: [] }); }); it('handles files that do not exist gracefully', () => { @@ -61,9 +61,9 @@ describe('getDeclarationNodesForPluginScope', () => { }, }; - const nodes = getDeclarationNodesForPluginScope(project, plugin, ApiScope.CLIENT, log); + const result = getDeclarationNodesForPluginScope(project, plugin, ApiScope.CLIENT, log); - // Should return empty array when file doesn't exist - expect(nodes).toEqual([]); + // Should return empty result when file doesn't exist + expect(result).toEqual({ nodes: [], unnamedExports: [] }); }); }); diff --git a/packages/kbn-docs-utils/src/get_declaration_nodes_for_plugin.ts b/packages/kbn-docs-utils/src/get_declaration_nodes_for_plugin.ts index cb306c4bec2e5..741033066e651 100644 --- a/packages/kbn-docs-utils/src/get_declaration_nodes_for_plugin.ts +++ b/packages/kbn-docs-utils/src/get_declaration_nodes_for_plugin.ts @@ -10,9 +10,17 @@ import Path from 'path'; import type { ToolingLog } from '@kbn/tooling-log'; import type { Project, SourceFile, Node } from 'ts-morph'; -import type { ApiScope, PluginOrPackage } from './types'; +import { type ApiScope, type PluginOrPackage, type UnnamedExport } from './types'; import { isNamedNode, getSourceFileMatching } from './tsmorph_utils'; +/** + * Result of collecting declaration nodes, including any unnamed exports found. + */ +export interface DeclarationNodesResult { + nodes: Node[]; + unnamedExports: UnnamedExport[]; +} + /** * Determines which file in the project to grab nodes from, depending on the plugin and scope, then returns those nodes. * @@ -21,17 +29,20 @@ import { isNamedNode, getSourceFileMatching } from './tsmorph_utils'; * @param scope - The "scope" of the API we want to extract: public, server or common. * @param log - logging utility. * - * @return Every publically exported Node from the given plugin and scope (public, server, common). + * @return Every publicly exported Node from the given plugin and scope (public, server, common), + * along with any unnamed exports that were encountered. */ export function getDeclarationNodesForPluginScope( project: Project, plugin: PluginOrPackage, scope: ApiScope, log: ToolingLog -): Node[] { +): DeclarationNodesResult { // Packages specify the intended scope in the package.json, while plugins specify the scope // using folder structure. - if (!plugin.isPlugin && scope !== plugin.scope) return []; + if (!plugin.isPlugin && scope !== plugin.scope) { + return { nodes: [], unnamedExports: [] }; + } const path = plugin.isPlugin ? Path.join(`${plugin.directory}`, scope.toString(), 'index.ts') @@ -39,20 +50,30 @@ export function getDeclarationNodesForPluginScope( const file = getSourceFileMatching(project, path); if (file) { - return getExportedFileDeclarations(file, log); + return getExportedFileDeclarations(file, plugin.id, scope, log); } else { log.debug(`No file found: ${path}`); - return []; + return { nodes: [], unnamedExports: [] }; } } /** + * Extracts exported declaration nodes from a source file. * - * @param source the file we want to extract exported declaration nodes from. - * @param log + * @param source - The file we want to extract exported declaration nodes from. + * @param pluginId - The plugin or package ID for tracking unnamed exports. + * @param scope - The API scope (client, server, common). + * @param log - Logging utility. + * @returns The extracted nodes and any unnamed exports encountered. */ -function getExportedFileDeclarations(source: SourceFile, log: ToolingLog): Node[] { +function getExportedFileDeclarations( + source: SourceFile, + pluginId: string, + scope: ApiScope, + log: ToolingLog +): DeclarationNodesResult { const nodes: Node[] = []; + const unnamedExports: UnnamedExport[] = []; const exported = source.getExportedDeclarations(); // Filter out the exported declarations that exist only for the plugin system itself. @@ -70,11 +91,29 @@ function getExportedFileDeclarations(source: SourceFile, log: ToolingLog): Node[ if (name && name !== '') { nodes.push(ed); } else { - log.warning(`API with missing name encountered, text is ` + ed.getText().substring(0, 50)); + const filePath = source.getFilePath(); + const lineNumber = ed.getStartLineNumber(); + const textSnippet = ed.getText().substring(0, 100).replace(/\n/g, ' '); + unnamedExports.push({ + pluginId, + scope, + path: filePath, + lineNumber, + textSnippet, + }); + log.debug( + `Unnamed export in ${pluginId} at ${filePath}:${lineNumber}: ${textSnippet.substring( + 0, + 50 + )}` + ); } }); }); log.debug(`Collected ${nodes.length} exports from file ${source.getFilePath()}`); - return nodes; + if (unnamedExports.length > 0) { + log.debug(`Found ${unnamedExports.length} unnamed exports in ${source.getFilePath()}`); + } + return { nodes, unnamedExports }; } diff --git a/packages/kbn-docs-utils/src/get_plugin_api.ts b/packages/kbn-docs-utils/src/get_plugin_api.ts index 04eac17794580..07003e5ba3eb0 100644 --- a/packages/kbn-docs-utils/src/get_plugin_api.ts +++ b/packages/kbn-docs-utils/src/get_plugin_api.ts @@ -10,13 +10,28 @@ import Path from 'path'; import type { Node, Project } from 'ts-morph'; import type { ToolingLog } from '@kbn/tooling-log'; -import type { PluginOrPackage } from './types'; +import type { PluginOrPackage, UnnamedExport } from './types'; import { ApiScope, Lifecycle } from './types'; import type { ApiDeclaration, PluginApi } from './types'; import { buildApiDeclarationTopNode } from './build_api_declarations/build_api_declaration'; import { getDeclarationNodesForPluginScope } from './get_declaration_nodes_for_plugin'; import { getSourceFileMatching } from './tsmorph_utils'; +/** + * Warnings encountered during plugin API collection. + */ +export interface PluginApiWarnings { + unnamedExports: UnnamedExport[]; +} + +/** + * Result of collecting plugin API, including any warnings found. + */ +export interface PluginApiResult { + pluginApi: PluginApi; + warnings: PluginApiWarnings; +} + /** * Collects all the information necessary to generate this plugins mdx api file(s). */ @@ -26,23 +41,60 @@ export function getPluginApi( plugins: PluginOrPackage[], log: ToolingLog, captureReferences: boolean -): PluginApi { - const client = getDeclarations(project, plugin, ApiScope.CLIENT, plugins, log, captureReferences); - const server = getDeclarations(project, plugin, ApiScope.SERVER, plugins, log, captureReferences); - const common = getDeclarations(project, plugin, ApiScope.COMMON, plugins, log, captureReferences); +): PluginApiResult { + const clientResult = getDeclarations( + project, + plugin, + ApiScope.CLIENT, + plugins, + log, + captureReferences + ); + const serverResult = getDeclarations( + project, + plugin, + ApiScope.SERVER, + plugins, + log, + captureReferences + ); + const commonResult = getDeclarations( + project, + plugin, + ApiScope.COMMON, + plugins, + log, + captureReferences + ); + + const unnamedExports = [ + ...clientResult.unnamedExports, + ...serverResult.unnamedExports, + ...commonResult.unnamedExports, + ]; + return { - id: plugin.id, - client, - server, - common, - serviceFolders: plugin.manifest.serviceFolders, + pluginApi: { + id: plugin.id, + client: clientResult.declarations, + server: serverResult.declarations, + common: commonResult.declarations, + serviceFolders: plugin.manifest.serviceFolders, + }, + warnings: { + unnamedExports, + }, }; } +interface DeclarationResult { + declarations: ApiDeclaration[]; + unnamedExports: UnnamedExport[]; +} + /** - * - * @returns All exported ApiDeclarations for the given plugin and scope (client, server, common), broken into - * groups of typescript kinds (functions, classes, interfaces, etc). + * Returns all exported ApiDeclarations for the given plugin and scope (client, server, common), + * along with any unnamed exports that were encountered. */ function getDeclarations( project: Project, @@ -51,8 +103,8 @@ function getDeclarations( plugins: PluginOrPackage[], log: ToolingLog, captureReferences: boolean -): ApiDeclaration[] { - const nodes = getDeclarationNodesForPluginScope(project, plugin, scope, log); +): DeclarationResult { + const { nodes, unnamedExports } = getDeclarationNodesForPluginScope(project, plugin, scope, log); const contractTypes = getContractTypes(project, plugin, scope); @@ -78,7 +130,7 @@ function getDeclarations( }, []); // We have all the ApiDeclarations, now lets group them by typescript kinds. - return declarations; + return { declarations, unnamedExports }; } /** diff --git a/packages/kbn-docs-utils/src/get_plugin_api_map.ts b/packages/kbn-docs-utils/src/get_plugin_api_map.ts index 0a760bdd7c46d..9cd53f1d0f960 100644 --- a/packages/kbn-docs-utils/src/get_plugin_api_map.ts +++ b/packages/kbn-docs-utils/src/get_plugin_api_map.ts @@ -18,6 +18,7 @@ import type { PluginOrPackage, ReferencedDeprecationsByPlugin, UnreferencedDeprecationsByPlugin, + UnnamedExportsByPlugin, } from './types'; import { removeBrokenLinks } from './utils'; import type { AdoptionTrackedAPIStats } from './types'; @@ -33,13 +34,20 @@ export function getPluginApiMap( referencedDeprecations: ReferencedDeprecationsByPlugin; unreferencedDeprecations: UnreferencedDeprecationsByPlugin; adoptionTrackedAPIs: AdoptionTrackedAPIsByPlugin; + unnamedExports: UnnamedExportsByPlugin; } { log.debug('Building plugin API map, getting missing comments, and collecting deprecations...'); const pluginApiMap: { [key: string]: PluginApi } = {}; + const unnamedExports: UnnamedExportsByPlugin = {}; + plugins.forEach((plugin) => { const captureReferences = collectReferences && (!pluginFilter || pluginFilter.indexOf(plugin.id) >= 0); - pluginApiMap[plugin.id] = getPluginApi(project, plugin, plugins, log, captureReferences); + const { pluginApi, warnings } = getPluginApi(project, plugin, plugins, log, captureReferences); + pluginApiMap[plugin.id] = pluginApi; + if (warnings.unnamedExports.length > 0) { + unnamedExports[plugin.id] = warnings.unnamedExports; + } }); // Mapping of plugin id to the missing source API id to all the plugin API items that referenced this item. @@ -61,6 +69,7 @@ export function getPluginApiMap( referencedDeprecations, unreferencedDeprecations, adoptionTrackedAPIs, + unnamedExports, }; } diff --git a/packages/kbn-docs-utils/src/integration_tests/api_doc_suite.test.ts b/packages/kbn-docs-utils/src/integration_tests/api_doc_suite.test.ts index 1da8607ffc720..53f623918aeed 100644 --- a/packages/kbn-docs-utils/src/integration_tests/api_doc_suite.test.ts +++ b/packages/kbn-docs-utils/src/integration_tests/api_doc_suite.test.ts @@ -119,8 +119,13 @@ beforeAll(async () => { pluginA.manifest.serviceFolders = ['foo']; const plugins: PluginOrPackage[] = [pluginA, pluginB]; - const { pluginApiMap, missingApiItems, referencedDeprecations, adoptionTrackedAPIs } = - getPluginApiMap(project, plugins, log, { collectReferences: false }); + const { + pluginApiMap, + missingApiItems, + referencedDeprecations, + adoptionTrackedAPIs, + unnamedExports, + } = getPluginApiMap(project, plugins, log, { collectReferences: false }); doc = pluginApiMap.pluginA; @@ -128,11 +133,13 @@ beforeAll(async () => { missingApiItems, referencedDeprecations, adoptionTrackedAPIs, + unnamedExports, }); pluginBStats = collectApiStatsForPlugin(pluginApiMap.pluginB, { missingApiItems, referencedDeprecations, adoptionTrackedAPIs, + unnamedExports, }); mdxOutputFolder = Path.resolve(__dirname, 'snapshots'); @@ -156,12 +163,22 @@ beforeAll(async () => { missingComplexTypeInfo: pluginAStats.missingComplexTypeInfo.length, isAnyType: pluginAStats.isAnyType.length, noReferences: pluginAStats.noReferences.length, + unnamedExports: pluginAStats.unnamedExports.length, }, missingComments: pluginAStats.missingComments.map(mapStat), paramDocMismatches: pluginAStats.paramDocMismatches.map(mapStat), missingComplexTypeInfo: pluginAStats.missingComplexTypeInfo.map(mapStat), isAnyType: pluginAStats.isAnyType.map(mapStat), noReferences: pluginAStats.noReferences.map(mapStat), + unnamedExports: pluginAStats.unnamedExports.map( + ({ pluginId, scope, path, lineNumber, textSnippet }) => ({ + pluginId, + scope, + path, + lineNumber, + textSnippet, + }) + ), }; fs.writeFileSync( Path.resolve(mdxOutputFolder, 'plugin_a.stats.json'), diff --git a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.stats.json b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.stats.json index 9ab45220e4a32..c840a22f1c4c7 100644 --- a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.stats.json +++ b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.stats.json @@ -6,7 +6,8 @@ "paramDocMismatches": 13, "missingComplexTypeInfo": 15, "isAnyType": 1, - "noReferences": 135 + "noReferences": 135, + "unnamedExports": 0 }, "missingComments": [ { @@ -1841,5 +1842,6 @@ "lineNumber": 12, "columnNumber": 1 } - ] + ], + "unnamedExports": [] } diff --git a/packages/kbn-docs-utils/src/mdx/split_apis_by_folder.test.ts b/packages/kbn-docs-utils/src/mdx/split_apis_by_folder.test.ts index ccb81a5797c21..e882254b300b6 100644 --- a/packages/kbn-docs-utils/src/mdx/split_apis_by_folder.test.ts +++ b/packages/kbn-docs-utils/src/mdx/split_apis_by_folder.test.ts @@ -38,7 +38,8 @@ beforeAll(() => { pluginA.manifest.serviceFolders = ['foo']; const plugins: PluginOrPackage[] = [pluginA]; - doc = getPluginApi(project, plugins[0], plugins, log, false); + const { pluginApi } = getPluginApi(project, plugins[0], plugins, log, false); + doc = pluginApi; }); test('foo service has all exports', () => { diff --git a/packages/kbn-docs-utils/src/mdx/write_plugin_split_by_folder.test.ts b/packages/kbn-docs-utils/src/mdx/write_plugin_split_by_folder.test.ts index bbbdb94e920b4..862b0fc120387 100644 --- a/packages/kbn-docs-utils/src/mdx/write_plugin_split_by_folder.test.ts +++ b/packages/kbn-docs-utils/src/mdx/write_plugin_split_by_folder.test.ts @@ -56,7 +56,7 @@ export interface Zed = { zed: string }` }, ]; - const doc = getPluginApi(project, plugins[0], plugins, log, false); + const { pluginApi: doc } = getPluginApi(project, plugins[0], plugins, log, false); const docs = splitApisByFolder(doc); // The api at the main level, and one on a service level. diff --git a/packages/kbn-docs-utils/src/stats.ts b/packages/kbn-docs-utils/src/stats.ts index 217aa7f0d9407..d21677db3b5b1 100644 --- a/packages/kbn-docs-utils/src/stats.ts +++ b/packages/kbn-docs-utils/src/stats.ts @@ -20,7 +20,7 @@ import { * Collects API stats for a single plugin. */ export function collectApiStatsForPlugin(doc: PluginApi, issues: IssuesByPlugin): ApiStats { - const { missingApiItems, referencedDeprecations, adoptionTrackedAPIs } = issues; + const { missingApiItems, referencedDeprecations, adoptionTrackedAPIs, unnamedExports } = issues; const stats: ApiStats = { missingComments: [], @@ -35,6 +35,7 @@ export function collectApiStatsForPlugin(doc: PluginApi, issues: IssuesByPlugin) adoptionTrackedAPIsUnreferencedCount: 0, apiCount: countApiForPlugin(doc), missingExports: Object.values(missingApiItems[doc.id] ?? {}).length, + unnamedExports: unnamedExports?.[doc.id] || [], }; Object.values(doc.client).forEach((def) => { collectStatsForApi(def, stats, doc); diff --git a/packages/kbn-docs-utils/src/types.ts b/packages/kbn-docs-utils/src/types.ts index f25bc3ae0151e..deba9c512e6c4 100644 --- a/packages/kbn-docs-utils/src/types.ts +++ b/packages/kbn-docs-utils/src/types.ts @@ -308,6 +308,30 @@ export interface ApiStats { * Number of adoption-tracked APIs that are still not referenced. */ adoptionTrackedAPIsUnreferencedCount: number; + /** + * Unnamed exports found in this plugin (e.g., JSDoc comments above non-declarations). + */ + unnamedExports: UnnamedExport[]; +} + +/** + * Represents an exported declaration that has no identifiable name. + * This typically occurs when a JSDoc-style comment appears above a line + * that isn't a proper declaration. + */ +export interface UnnamedExport { + pluginId: string; + scope: ApiScope; + path: string; + lineNumber: number; + textSnippet: string; +} + +/** + * A mapping of plugin id to a list of unnamed exports found in that plugin. + */ +export interface UnnamedExportsByPlugin { + [pluginId: string]: UnnamedExport[]; } /** @@ -318,6 +342,7 @@ export interface IssuesByPlugin { missingApiItems: MissingApiItemMap; referencedDeprecations: ReferencedDeprecationsByPlugin; adoptionTrackedAPIs: AdoptionTrackedAPIsByPlugin; + unnamedExports?: UnnamedExportsByPlugin; } export type PluginMetaInfo = ApiStats & { diff --git a/packages/kbn-docs-utils/src/utils.test.ts b/packages/kbn-docs-utils/src/utils.test.ts index 706e8cdaf8959..2f3dfdce9c5a6 100644 --- a/packages/kbn-docs-utils/src/utils.test.ts +++ b/packages/kbn-docs-utils/src/utils.test.ts @@ -96,7 +96,7 @@ it('test removeBrokenLinks', () => { const pluginApiMap: { [key: string]: PluginApi } = {}; plugins.map((plugin) => { - pluginApiMap[plugin.id] = getPluginApi(project, plugin, plugins, log, false); + pluginApiMap[plugin.id] = getPluginApi(project, plugin, plugins, log, false).pluginApi; }); const missingApiItems: { [key: string]: { [key: string]: string[] } } = {};