From b45340896108f38ae6321d002d343795e2d80eb7 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Wed, 31 Dec 2025 10:40:46 -0500 Subject: [PATCH 1/4] [kbn-docs-utils] Add unit tests and improve API declaration metadata Add comprehensive unit test coverage for kbn-docs-utils and enhance API declarations with source location metadata. API declaration improvements: - Add lineNumber and columnNumber to ApiDeclaration type - Add getSourceLocationForNode() utility for source positions - Update buildBasicApiDeclaration to capture line/column info Test infrastructure: - Configure Jest coverage collection for src/**/*.{ts,tsx} - Add stats snapshot generation alongside other test snapshots - Add update_fixture_comments.js script for syncing expected issues comments in fixture files Fixture updates: - Expand test fixtures with additional API patterns - Add expected issues comments to fixture files - Regenerate snapshot files with new metadata --- packages/kbn-docs-utils/jest.config.js | 7 + .../kbn-docs-utils/jest.integration.config.js | 6 + .../scripts/update_fixture_comments.js | 115 ++ .../src/__test_helpers__/mocks.ts | 109 ++ .../buid_api_declaration.test.ts | 171 -- .../build_api_declaration.test.ts | 695 +++++++ .../build_basic_api_declaration.ts | 8 +- .../extract_import_refs.test.ts | 14 + .../js_doc_utils.test.ts | 396 ++++ .../src/build_api_declarations/utils.ts | 14 + .../src/build_api_docs_cli.test.ts | 77 + .../src/count_eslint_disable.test.ts | 2 +- .../kbn-docs-utils/src/find_plugins.test.ts | 109 ++ .../get_declaration_nodes_for_plugin.test.ts | 69 + .../src/get_paths_by_package.test.ts | 124 ++ .../src/get_plugin_api_map.test.ts | 137 ++ .../src/plugin_a/common/foo/index.ts | 6 + .../__fixtures__/src/plugin_a/common/index.ts | 8 + .../src/plugin_a/public/classes.ts | 36 + .../src/plugin_a/public/const_vars.ts | 20 + .../__fixtures__/src/plugin_a/public/fns.ts | 71 + .../src/plugin_a/public/foo/index.ts | 8 + .../__fixtures__/src/plugin_a/public/index.ts | 24 + .../src/plugin_a/public/plugin.ts | 37 + .../__fixtures__/src/plugin_a/public/types.ts | 39 + .../integration_tests/api_doc_suite.test.ts | 331 ++++ .../snapshots/plugin_a.devdocs.json | 268 ++- .../integration_tests/snapshots/plugin_a.mdx | 2 +- .../snapshots/plugin_a.stats.json | 1711 +++++++++++++++++ .../snapshots/plugin_a_foo.devdocs.json | 8 +- .../snapshots/plugin_a_foo.mdx | 2 +- .../snapshots/plugin_b.devdocs.json | 6 +- .../integration_tests/snapshots/plugin_b.mdx | 2 +- .../build_plugin_deprecations_table.test.ts | 266 +++ .../mdx/build_plugin_deprecations_table.ts | 12 +- .../src/mdx/get_all_doc_file_ids.test.ts | 159 ++ .../mdx/write_deprecations_doc_by_api.test.ts | 232 +++ .../write_deprecations_doc_by_plugin.test.ts | 230 +++ .../mdx/write_deprecations_doc_by_plugin.ts | 5 +- .../write_deprecations_due_by_team.test.ts | 344 ++++ .../src/mdx/write_deprecations_due_by_team.ts | 5 +- .../mdx/write_plugin_directory_doc.test.ts | 300 +++ .../src/mdx/write_plugin_mdx_docs.test.ts | 353 ++++ packages/kbn-docs-utils/src/stats.test.ts | 700 +++++++ .../src/trim_deleted_docs_from_nav.test.ts | 166 ++ packages/kbn-docs-utils/src/types.ts | 10 + packages/kbn-docs-utils/src/utils.test.ts | 390 +++- 47 files changed, 7612 insertions(+), 192 deletions(-) create mode 100644 packages/kbn-docs-utils/scripts/update_fixture_comments.js create mode 100644 packages/kbn-docs-utils/src/__test_helpers__/mocks.ts delete mode 100644 packages/kbn-docs-utils/src/build_api_declarations/buid_api_declaration.test.ts create mode 100644 packages/kbn-docs-utils/src/build_api_declarations/build_api_declaration.test.ts create mode 100644 packages/kbn-docs-utils/src/build_api_declarations/js_doc_utils.test.ts create mode 100644 packages/kbn-docs-utils/src/build_api_docs_cli.test.ts create mode 100644 packages/kbn-docs-utils/src/find_plugins.test.ts create mode 100644 packages/kbn-docs-utils/src/get_declaration_nodes_for_plugin.test.ts create mode 100644 packages/kbn-docs-utils/src/get_paths_by_package.test.ts create mode 100644 packages/kbn-docs-utils/src/get_plugin_api_map.test.ts create mode 100644 packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.stats.json create mode 100644 packages/kbn-docs-utils/src/mdx/build_plugin_deprecations_table.test.ts create mode 100644 packages/kbn-docs-utils/src/mdx/get_all_doc_file_ids.test.ts create mode 100644 packages/kbn-docs-utils/src/mdx/write_deprecations_doc_by_api.test.ts create mode 100644 packages/kbn-docs-utils/src/mdx/write_deprecations_doc_by_plugin.test.ts create mode 100644 packages/kbn-docs-utils/src/mdx/write_deprecations_due_by_team.test.ts create mode 100644 packages/kbn-docs-utils/src/mdx/write_plugin_directory_doc.test.ts create mode 100644 packages/kbn-docs-utils/src/mdx/write_plugin_mdx_docs.test.ts create mode 100644 packages/kbn-docs-utils/src/stats.test.ts create mode 100644 packages/kbn-docs-utils/src/trim_deleted_docs_from_nav.test.ts diff --git a/packages/kbn-docs-utils/jest.config.js b/packages/kbn-docs-utils/jest.config.js index 3e34189a436fa..54dd1bb144d46 100644 --- a/packages/kbn-docs-utils/jest.config.js +++ b/packages/kbn-docs-utils/jest.config.js @@ -11,4 +11,11 @@ module.exports = { preset: '@kbn/test', rootDir: '../..', roots: ['/packages/kbn-docs-utils'], + coverageDirectory: '/target/kibana-coverage/jest/packages/kbn-docs-utils', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/packages/kbn-docs-utils/src/**/*.{ts,tsx}', + '!/packages/kbn-docs-utils/src/**/*.test.ts', + '!/packages/kbn-docs-utils/src/**/__fixtures__/**', + ], }; diff --git a/packages/kbn-docs-utils/jest.integration.config.js b/packages/kbn-docs-utils/jest.integration.config.js index c5c200b0cf06d..59d4ba92ed9b2 100644 --- a/packages/kbn-docs-utils/jest.integration.config.js +++ b/packages/kbn-docs-utils/jest.integration.config.js @@ -11,4 +11,10 @@ module.exports = { preset: '@kbn/test/jest_integration_node', rootDir: '../..', roots: ['/packages/kbn-docs-utils'], + coverageDirectory: '/target/kibana-coverage/jest/packages/kbn-docs-utils', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/packages/kbn-docs-utils/src/**/*.{ts,tsx}', + '!/packages/kbn-docs-utils/src/**/*.test.ts', + ], }; diff --git a/packages/kbn-docs-utils/scripts/update_fixture_comments.js b/packages/kbn-docs-utils/scripts/update_fixture_comments.js new file mode 100644 index 0000000000000..d40b83562d082 --- /dev/null +++ b/packages/kbn-docs-utils/scripts/update_fixture_comments.js @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const categories = [ + { key: 'missingComments', title: 'missing comments' }, + { key: 'isAnyType', title: 'any usage' }, + { key: 'noReferences', title: 'no references' }, +]; + +/** + * Reads stats from the snapshot JSON file. + */ +const readStatsFromSnapshot = () => { + const snapshotPath = path.resolve( + __dirname, + '../src/integration_tests/snapshots/plugin_a.stats.json' + ); + + if (!fs.existsSync(snapshotPath)) { + console.error(`Stats snapshot file not found: ${snapshotPath}`); + console.error('Run the integration tests first to generate the snapshot.'); + process.exit(1); + } + + return JSON.parse(fs.readFileSync(snapshotPath, 'utf8')); +}; + +const normalizePath = (statPath) => + path.resolve(__dirname, '..', statPath.replace(/^packages\/kbn-docs-utils\//, '')); + +const emptyCategories = () => + categories.reduce((acc, { key }) => { + acc[key] = []; + return acc; + }, {}); + +const groupByFile = (stats) => { + const byFile = new Map(); + categories.forEach(({ key }) => { + const entries = stats[key] || []; + entries.forEach((entry) => { + const absPath = normalizePath(entry.path); + if (!byFile.has(absPath)) byFile.set(absPath, emptyCategories()); + byFile.get(absPath)[key].push(entry); + }); + }); + return byFile; +}; + +const formatCategory = (title, entries) => { + const count = entries.length; + const lines = [`// ${title} (${count}):`]; + 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; + }); + sorted.forEach((entry) => { + const lineInfo = entry.lineNumber != null ? `line ${entry.lineNumber}` : 'unknown line'; + lines.push(`// ${lineInfo} - ${entry.label}`); + }); + return lines.join('\n'); +}; + +const buildBlock = (fileStats) => { + const parts = ['// Expected issues:']; + let added = false; + categories.forEach(({ key, title }) => { + const entries = fileStats[key] || []; + if (!entries.length) return; + parts.push(formatCategory(title, entries)); + added = true; + }); + if (!added) { + parts.push('// none'); + } + return `${parts.join('\n')}\n`; +}; + +const replaceBlock = (content, block) => { + const marker = '// Expected issues:'; + const idx = content.lastIndexOf(marker); + if (idx === -1) { + const trimmed = content.endsWith('\n') ? content : `${content}\n`; + return `${trimmed}${block}`; + } + return `${content.slice(0, idx)}${block}`; +}; + +const main = () => { + const stats = readStatsFromSnapshot(); + const byFile = groupByFile(stats); + + byFile.forEach((fileStats, absPath) => { + if (!fs.existsSync(absPath)) return; + const original = fs.readFileSync(absPath, 'utf8'); + const block = buildBlock(fileStats); + const next = replaceBlock(original, block); + if (next !== original) { + fs.writeFileSync(absPath, next, 'utf8'); + console.log(`Updated: ${absPath}`); + } + }); +}; + +main(); diff --git a/packages/kbn-docs-utils/src/__test_helpers__/mocks.ts b/packages/kbn-docs-utils/src/__test_helpers__/mocks.ts new file mode 100644 index 0000000000000..33af4f7caba9b --- /dev/null +++ b/packages/kbn-docs-utils/src/__test_helpers__/mocks.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { + ApiDeclaration, + ApiReference, + ApiStats, + PluginApi, + PluginMetaInfo, + PluginOrPackage, +} from '../types'; +import { TypeKind } from '../types'; + +/** + * Creates a mock `ApiDeclaration` for testing purposes. + */ +export const createMockApiDeclaration = ( + overrides: Partial = {} +): ApiDeclaration => ({ + id: 'testApi', + label: 'testApi', + type: TypeKind.FunctionKind, + path: 'src/plugins/test/public/index.ts', + parentPluginId: 'testPlugin', + ...overrides, +}); + +/** + * Creates a mock `ApiReference` for testing purposes. + */ +export const createMockReference = (overrides: Partial = {}): ApiReference => ({ + plugin: 'referencingPlugin', + path: 'src/plugins/referencing/public/file.ts', + ...overrides, +}); + +/** + * Creates a mock `PluginOrPackage` for testing purposes. + */ +export const createMockPlugin = (overrides: Partial = {}): PluginOrPackage => ({ + id: 'testPlugin', + manifest: { + id: 'testPlugin', + description: 'A test plugin', + owner: { name: 'Test Team', githubTeam: 'test-team' }, + serviceFolders: [], + }, + isPlugin: true, + directory: 'src/plugins/test', + manifestPath: 'src/plugins/test/kibana.jsonc', + ...overrides, +}); + +/** + * Creates a mock `ApiStats` for testing purposes. + */ +export const createMockPluginStats = (overrides: Partial = {}): ApiStats => ({ + apiCount: 10, + missingComments: [], + isAnyType: [], + noReferences: [], + missingExports: 0, + deprecatedAPIsReferencedCount: 0, + unreferencedDeprecatedApisCount: 0, + adoptionTrackedAPIs: [], + adoptionTrackedAPIsCount: 0, + adoptionTrackedAPIsUnreferencedCount: 0, + ...overrides, +}); + +/** + * Creates a mock `PluginApi` for testing purposes. + */ +export const createMockPluginApi = (overrides: Partial = {}): PluginApi => ({ + id: 'testPlugin', + client: [], + server: [], + common: [], + ...overrides, +}); + +/** + * Creates a mock `PluginMetaInfo` for testing purposes. + */ +export const createMockPluginMetaInfo = ( + overrides: Partial = {} +): PluginMetaInfo => ({ + apiCount: 10, + missingComments: [], + isAnyType: [], + noReferences: [], + missingExports: 0, + deprecatedAPIsReferencedCount: 0, + unreferencedDeprecatedApisCount: 0, + adoptionTrackedAPIs: [], + adoptionTrackedAPIsCount: 0, + adoptionTrackedAPIsUnreferencedCount: 0, + owner: { name: 'Test Team', githubTeam: 'test-team' }, + description: 'A test plugin', + isPlugin: true, + ...overrides, +}); + diff --git a/packages/kbn-docs-utils/src/build_api_declarations/buid_api_declaration.test.ts b/packages/kbn-docs-utils/src/build_api_declarations/buid_api_declaration.test.ts deleted file mode 100644 index b906d6c2343fb..0000000000000 --- a/packages/kbn-docs-utils/src/build_api_declarations/buid_api_declaration.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import Path from 'path'; -import type { Node } from 'ts-morph'; -import { Project } from 'ts-morph'; -import { ToolingLog } from '@kbn/tooling-log'; - -import type { PluginOrPackage } from '../types'; -import { TypeKind, ApiScope } from '../types'; -import { getKibanaPlatformPlugin } from '../integration_tests/kibana_platform_plugin_mock'; -import { getDeclarationNodesForPluginScope } from '../get_declaration_nodes_for_plugin'; -import { buildApiDeclarationTopNode } from './build_api_declaration'; -import { isNamedNode } from '../tsmorph_utils'; - -const log = new ToolingLog({ - level: 'debug', - writeTo: process.stdout, -}); - -let nodes: Node[]; -let plugins: PluginOrPackage[]; - -function getNodeName(node: Node): string { - return isNamedNode(node) ? node.getName() : ''; -} - -beforeAll(() => { - const tsConfigFilePath = Path.resolve( - __dirname, - '../integration_tests/__fixtures__/src/tsconfig.json' - ); - const project = new Project({ - tsConfigFilePath, - }); - - plugins = [getKibanaPlatformPlugin('pluginA')]; - - nodes = getDeclarationNodesForPluginScope(project, plugins[0], ApiScope.CLIENT, log); -}); - -it('Test number primitive doc def', () => { - const node = nodes.find((n) => getNodeName(n) === 'aNum'); - expect(node).toBeDefined(); - const def = buildApiDeclarationTopNode(node!, { - plugins, - log, - currentPluginId: plugins[0].id, - scope: ApiScope.CLIENT, - captureReferences: false, - }); - - expect(def.type).toBe(TypeKind.NumberKind); -}); - -it('Test a constructor type declaration inside an interface', () => { - const node = nodes.find((n) => getNodeName(n) === 'ClassConstructorWithStaticProperties'); - expect(node).toBeDefined(); - const def = buildApiDeclarationTopNode(node!, { - plugins, - log, - currentPluginId: plugins[0].id, - scope: ApiScope.CLIENT, - captureReferences: false, - }); - - expect(def.type).toBe(TypeKind.InterfaceKind); - expect(def.children).toHaveLength(2); - expect(def.children![1].type).toBe(TypeKind.FunctionKind); - expect(def.children![1].label).toBe('new'); - expect(def.children![1].id).toBe('def-public.ClassConstructorWithStaticProperties.new'); -}); - -it('Function type is exported as type with signature', () => { - const node = nodes.find((n) => getNodeName(n) === 'FnWithGeneric'); - expect(node).toBeDefined(); - const def = buildApiDeclarationTopNode(node!, { - plugins, - log, - currentPluginId: plugins[0].id, - scope: ApiScope.CLIENT, - captureReferences: false, - }); - expect(def).toBeDefined(); - expect(def?.type).toBe(TypeKind.TypeKind); - expect(def?.signature?.length).toBeGreaterThan(0); -}); - -it('Test Interface Kind doc def', () => { - const node = nodes.find((n) => getNodeName(n) === 'ExampleInterface'); - expect(node).toBeDefined(); - const def = buildApiDeclarationTopNode(node!, { - plugins, - log, - currentPluginId: plugins[0].id, - scope: ApiScope.CLIENT, - captureReferences: false, - }); - - expect(def.type).toBe(TypeKind.InterfaceKind); - expect(def.children).toBeDefined(); - expect(def.children!.length).toBe(6); -}); - -it('Test union export', () => { - const node = nodes.find((n) => getNodeName(n) === 'aUnionProperty'); - expect(node).toBeDefined(); - const def = buildApiDeclarationTopNode(node!, { - plugins, - log, - currentPluginId: plugins[0].id, - scope: ApiScope.CLIENT, - captureReferences: false, - }); - expect(def.type).toBe(TypeKind.CompoundTypeKind); -}); - -it('Function inside interface has a label', () => { - const node = nodes.find((n) => getNodeName(n) === 'ExampleInterface'); - expect(node).toBeDefined(); - const def = buildApiDeclarationTopNode(node!, { - plugins, - log, - currentPluginId: plugins[0].id, - scope: ApiScope.CLIENT, - captureReferences: false, - }); - - const fn = def!.children?.find((c) => c.label === 'aFn'); - expect(fn).toBeDefined(); - expect(fn?.label).toBe('aFn'); - expect(fn?.type).toBe(TypeKind.FunctionKind); -}); - -// FAILING: https://github.com/elastic/kibana/issues/120125 -it.skip('Test ReactElement signature', () => { - const node = nodes.find((n) => getNodeName(n) === 'AReactElementFn'); - expect(node).toBeDefined(); - const def = buildApiDeclarationTopNode(node!, { - plugins, - log, - currentPluginId: plugins[0].id, - scope: ApiScope.CLIENT, - captureReferences: false, - }); - expect(def.signature).toBeDefined(); - expect(def.signature!.length).toBe(3); - // There is a terrible hack to achieve this, but without it, ReactElement expands to include the second default generic type - // (ReactElement) and - // it looks awful. - expect(def.signature![2]).toBe('>'); - expect(def.signature!).toMatchInlineSnapshot(` - Array [ - "() => React.ReactElement<", - Object { - "docId": "kibPluginAPluginApi", - "pluginId": "pluginA", - "scope": "public", - "section": "def-public.MyProps", - "text": "MyProps", - }, - ">", - ] - `); -}); 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 new file mode 100644 index 0000000000000..949c1c828482d --- /dev/null +++ b/packages/kbn-docs-utils/src/build_api_declarations/build_api_declaration.test.ts @@ -0,0 +1,695 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import Path from 'path'; +import type { Node } from 'ts-morph'; +import { Project } from 'ts-morph'; +import { ToolingLog } from '@kbn/tooling-log'; + +import type { PluginOrPackage } from '../types'; +import { TypeKind, ApiScope } from '../types'; +import { getKibanaPlatformPlugin } from '../integration_tests/kibana_platform_plugin_mock'; +import { getDeclarationNodesForPluginScope } from '../get_declaration_nodes_for_plugin'; +import { buildApiDeclarationTopNode } from './build_api_declaration'; +import { isNamedNode } from '../tsmorph_utils'; +import { getTypeKind } from './get_type_kind'; +import { getSignature } from './get_signature'; +import { buildBasicApiDeclaration } from './build_basic_api_declaration'; +import { buildVariableDec } from './build_variable_dec'; +import { buildCallSignatureDec } from './build_call_signature_dec'; +import { getReferences } from './get_references'; + +const log = new ToolingLog({ + level: 'debug', + writeTo: process.stdout, +}); + +let nodes: Node[]; +let plugins: PluginOrPackage[]; + +function getNodeName(node: Node): string { + return isNamedNode(node) ? node.getName() : ''; +} + +beforeAll(() => { + const tsConfigFilePath = Path.resolve( + __dirname, + '../integration_tests/__fixtures__/src/tsconfig.json' + ); + const project = new Project({ + tsConfigFilePath, + }); + + plugins = [getKibanaPlatformPlugin('pluginA')]; + + nodes = getDeclarationNodesForPluginScope(project, plugins[0], ApiScope.CLIENT, log); +}); + +it('Test number primitive doc def', () => { + const node = nodes.find((n) => getNodeName(n) === 'aNum'); + expect(node).toBeDefined(); + const def = buildApiDeclarationTopNode(node!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + + expect(def.type).toBe(TypeKind.NumberKind); +}); + +it('Test a constructor type declaration inside an interface', () => { + const node = nodes.find((n) => getNodeName(n) === 'ClassConstructorWithStaticProperties'); + expect(node).toBeDefined(); + const def = buildApiDeclarationTopNode(node!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + + expect(def.type).toBe(TypeKind.InterfaceKind); + expect(def.children).toHaveLength(2); + expect(def.children![1].type).toBe(TypeKind.FunctionKind); + expect(def.children![1].label).toBe('new'); + expect(def.children![1].id).toBe('def-public.ClassConstructorWithStaticProperties.new'); +}); + +it('Function type is exported as type with signature', () => { + const node = nodes.find((n) => getNodeName(n) === 'FnWithGeneric'); + expect(node).toBeDefined(); + const def = buildApiDeclarationTopNode(node!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + expect(def).toBeDefined(); + expect(def?.type).toBe(TypeKind.TypeKind); + expect(def?.signature?.length).toBeGreaterThan(0); +}); + +it('Test Interface Kind doc def', () => { + const node = nodes.find((n) => getNodeName(n) === 'ExampleInterface'); + expect(node).toBeDefined(); + const def = buildApiDeclarationTopNode(node!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + + expect(def.type).toBe(TypeKind.InterfaceKind); + expect(def.children).toBeDefined(); + expect(def.children!.length).toBe(6); +}); + +it('Test union export', () => { + const node = nodes.find((n) => getNodeName(n) === 'aUnionProperty'); + expect(node).toBeDefined(); + const def = buildApiDeclarationTopNode(node!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + expect(def.type).toBe(TypeKind.CompoundTypeKind); +}); + +it('Function inside interface has a label', () => { + const node = nodes.find((n) => getNodeName(n) === 'ExampleInterface'); + expect(node).toBeDefined(); + const def = buildApiDeclarationTopNode(node!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + + const fn = def!.children?.find((c) => c.label === 'aFn'); + expect(fn).toBeDefined(); + expect(fn?.label).toBe('aFn'); + expect(fn?.type).toBe(TypeKind.FunctionKind); +}); + +// FAILING: https://github.com/elastic/kibana/issues/120125 +it.skip('Test ReactElement signature', () => { + const node = nodes.find((n) => getNodeName(n) === 'AReactElementFn'); + expect(node).toBeDefined(); + const def = buildApiDeclarationTopNode(node!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + expect(def.signature).toBeDefined(); + expect(def.signature!.length).toBe(3); + // There is a terrible hack to achieve this, but without it, ReactElement expands to include the second default generic type + // (ReactElement) and + // it looks awful. + expect(def.signature![2]).toBe('>'); + expect(def.signature!).toMatchInlineSnapshot(` + Array [ + "() => React.ReactElement<", + Object { + "docId": "kibPluginAPluginApi", + "pluginId": "pluginA", + "scope": "public", + "section": "def-public.MyProps", + "text": "MyProps", + }, + ">", + ] + `); +}); + +describe('Parameter extraction', () => { + it('extracts simple parameters with JSDoc comments', () => { + const node = nodes.find((n) => getNodeName(n) === 'notAnArrowFn'); + expect(node).toBeDefined(); + const def = buildApiDeclarationTopNode(node!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + + expect(def.type).toBe(TypeKind.FunctionKind); + expect(def.children).toBeDefined(); + expect(def.children!.length).toBe(5); + + // Check that parameters have descriptions from JSDoc + const paramA = def.children!.find((c) => c.label === 'a'); + expect(paramA).toBeDefined(); + expect(paramA!.description).toBeDefined(); + expect(paramA!.description!.length).toBeGreaterThan(0); + expect(paramA!.description![0]).toContain('letter A'); + + const paramB = def.children!.find((c) => c.label === 'b'); + expect(paramB).toBeDefined(); + expect(paramB!.description).toBeDefined(); + expect(paramB!.description!.length).toBeGreaterThan(0); + }); + + it('extracts destructured parameters as children', () => { + const node = nodes.find((n) => getNodeName(n) === 'crazyFunction'); + expect(node).toBeDefined(); + const def = buildApiDeclarationTopNode(node!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + + expect(def.type).toBe(TypeKind.FunctionKind); + expect(def.children).toBeDefined(); + expect(def.children!.length).toBe(3); + + // First parameter: obj: { hi: string } + const paramObj = def.children!.find((c) => c.label === 'obj'); + expect(paramObj).toBeDefined(); + expect(paramObj!.children).toBeDefined(); + expect(paramObj!.children!.length).toBe(1); + + const hiProp = paramObj!.children!.find((c) => c.label === 'hi'); + expect(hiProp).toBeDefined(); + expect(hiProp!.type).toBe(TypeKind.StringKind); + + // Second parameter: { fn1, fn2 }: { fn1: Function, fn2: Function } + const paramFn = def.children!.find((c) => c.label === '{ fn1, fn2 }'); + expect(paramFn).toBeDefined(); + expect(paramFn!.children).toBeDefined(); + expect(paramFn!.children!.length).toBe(2); + + const fn1 = paramFn!.children!.find((c) => c.label === 'fn1'); + expect(fn1).toBeDefined(); + expect(fn1!.type).toBe(TypeKind.FunctionKind); + + const fn2 = paramFn!.children!.find((c) => c.label === 'fn2'); + expect(fn2).toBeDefined(); + expect(fn2!.type).toBe(TypeKind.FunctionKind); + + // Third parameter: { str }: { str: string } + const paramStr = def.children!.find((c) => c.label === '{ str }'); + expect(paramStr).toBeDefined(); + expect(paramStr!.children).toBeDefined(); + expect(paramStr!.children!.length).toBe(1); + }); + + it('extracts nested destructured parameters', () => { + const node = nodes.find((n) => getNodeName(n) === 'crazyFunction'); + expect(node).toBeDefined(); + const def = buildApiDeclarationTopNode(node!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + + // Check nested structure: { fn1, fn2 }.fn1.foo.param + const paramFn = def.children!.find((c) => c.label === '{ fn1, fn2 }'); + expect(paramFn).toBeDefined(); + + const fn1 = paramFn!.children!.find((c) => c.label === 'fn1'); + expect(fn1).toBeDefined(); + expect(fn1!.type).toBe(TypeKind.FunctionKind); + expect(fn1!.children).toBeDefined(); + + // fn1 has a parameter: foo: { param: string } + const fooParam = fn1!.children!.find((c) => c.label === 'foo'); + expect(fooParam).toBeDefined(); + expect(fooParam!.type).toBe(TypeKind.ObjectKind); + expect(fooParam!.children).toBeDefined(); + + const paramProp = fooParam!.children!.find((c) => c.label === 'param'); + expect(paramProp).toBeDefined(); + expect(paramProp!.type).toBe(TypeKind.StringKind); + }); + + it('extracts parameter comments from JSDoc', () => { + const node = nodes.find((n) => getNodeName(n) === 'notAnArrowFn'); + expect(node).toBeDefined(); + const def = buildApiDeclarationTopNode(node!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + + // Check that parameters have descriptions extracted from @param tags + const paramA = def.children!.find((c) => c.label === 'a'); + expect(paramA!.description).toBeDefined(); + expect(paramA!.description!.length).toBeGreaterThan(0); + + const paramB = def.children!.find((c) => c.label === 'b'); + expect(paramB!.description).toBeDefined(); + expect(paramB!.description!.length).toBeGreaterThan(0); + expect(paramB!.description![0]).toContain('Feed me'); + }); + + it('handles parameters without JSDoc comments', () => { + const node = nodes.find((n) => getNodeName(n) === 'fnWithNonExportedRef'); + expect(node).toBeDefined(); + const def = buildApiDeclarationTopNode(node!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + + expect(def.type).toBe(TypeKind.FunctionKind); + expect(def.children).toBeDefined(); + expect(def.children!.length).toBe(1); + + const paramA = def.children!.find((c) => c.label === 'a'); + expect(paramA).toBeDefined(); + // Parameter without @param tag should have empty description + expect(paramA!.description).toBeDefined(); + expect(paramA!.description!.length).toBe(0); + }); + + it('extracts inline object parameters as children', () => { + // Test with getSearchService2 from plugin.ts which has inline object parameter + // Setup is an interface, so we need to find it and access its children + const setupNode = nodes.find((n) => { + if (isNamedNode(n) && n.getName() === 'Setup') { + return true; + } + return false; + }); + expect(setupNode).toBeDefined(); + + const setupDef = buildApiDeclarationTopNode(setupNode!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + + // getSearchService2 is a property of the Setup interface + const getSearchService2 = setupDef.children!.find((c) => c.label === 'getSearchService2'); + expect(getSearchService2).toBeDefined(); + expect(getSearchService2!.type).toBe(TypeKind.FunctionKind); + expect(getSearchService2!.children).toBeDefined(); + + // Inline object parameter should be extracted + const searchSpecParam = getSearchService2!.children!.find((c) => c.label === 'searchSpec'); + expect(searchSpecParam).toBeDefined(); + expect(searchSpecParam!.children).toBeDefined(); + expect(searchSpecParam!.children!.length).toBe(2); // username and password + + const usernameProp = searchSpecParam!.children!.find((c) => c.label === 'username'); + expect(usernameProp).toBeDefined(); + expect(usernameProp!.type).toBe(TypeKind.StringKind); + + const passwordProp = searchSpecParam!.children!.find((c) => c.label === 'password'); + expect(passwordProp).toBeDefined(); + expect(passwordProp!.type).toBe(TypeKind.StringKind); + }); + + it('handles deeply nested inline object parameters', () => { + // Test with fnWithInlineParams from plugin.ts + const setupNode = nodes.find((n) => { + if (isNamedNode(n) && n.getName() === 'Setup') { + return true; + } + return false; + }); + expect(setupNode).toBeDefined(); + + const setupDef = buildApiDeclarationTopNode(setupNode!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + + // fnWithInlineParams is a property of the Setup interface + const fnWithInlineParams = setupDef.children!.find((c) => c.label === 'fnWithInlineParams'); + expect(fnWithInlineParams).toBeDefined(); + expect(fnWithInlineParams!.type).toBe(TypeKind.FunctionKind); + expect(fnWithInlineParams!.children).toBeDefined(); + + // obj: { fn: (foo: { param: string }) => number } + const objParam = fnWithInlineParams!.children!.find((c) => c.label === 'obj'); + expect(objParam).toBeDefined(); + expect(objParam!.children).toBeDefined(); + + const fnProp = objParam!.children!.find((c) => c.label === 'fn'); + expect(fnProp).toBeDefined(); + expect(fnProp!.type).toBe(TypeKind.FunctionKind); + expect(fnProp!.children).toBeDefined(); + + // fn has parameter: foo: { param: string } + const fooParam = fnProp!.children!.find((c) => c.label === 'foo'); + expect(fooParam).toBeDefined(); + expect(fooParam!.type).toBe(TypeKind.ObjectKind); + expect(fooParam!.children).toBeDefined(); + + const paramProp = fooParam!.children!.find((c) => c.label === 'param'); + expect(paramProp).toBeDefined(); + expect(paramProp!.type).toBe(TypeKind.StringKind); + }); + + it('extracts parent parameter comment for destructured parameters', () => { + const node = nodes.find((n) => getNodeName(n) === 'crazyFunction'); + expect(node).toBeDefined(); + const def = buildApiDeclarationTopNode(node!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + + // First parameter has @param obj comment + const paramObj = def.children!.find((c) => c.label === 'obj'); + expect(paramObj).toBeDefined(); + // Current behavior: parent parameter comments are NOT extracted for TypeLiteral parameters + // This is a known limitation - when a parameter has a TypeLiteral type (destructured params), + // buildApiDeclaration is called directly without extracting the JSDoc comment for the parameter name. + // This will be fixed in Phase 4.1 + expect(paramObj!.description).toBeDefined(); + // Currently, the description is empty for destructured parameters + // After Phase 4.1, this should contain the @param obj comment + expect(paramObj!.description!.length).toBe(0); + }); + + it('does not extract property-level JSDoc comments (current limitation)', () => { + // This test documents current behavior: property-level @param tags like @param obj.hi + // are not currently extracted. This will be fixed in Phase 4.1. + const node = nodes.find((n) => getNodeName(n) === 'crazyFunction'); + expect(node).toBeDefined(); + const def = buildApiDeclarationTopNode(node!, { + plugins, + log, + currentPluginId: plugins[0].id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + + const paramObj = def.children!.find((c) => c.label === 'obj'); + expect(paramObj).toBeDefined(); + + const hiProp = paramObj!.children!.find((c) => c.label === 'hi'); + expect(hiProp).toBeDefined(); + // Current behavior: property-level comments are not extracted + // Even if @param obj.hi existed, it wouldn't be found + // This is a known limitation that will be addressed in Phase 4.1 + expect(hiProp!.description).toBeDefined(); + expect(hiProp!.description!.length).toBe(0); + }); +}); + +describe('buildBasicApiDeclaration edge cases', () => { + it('throws error for index signature with unexpected name', () => { + const project = new Project({ + useInMemoryFileSystem: true, + }); + + const sourceFile = project.createSourceFile( + 'test.ts', + ` + interface TestInterface { + [key: string]: number; + } + ` + ); + + const indexSig = sourceFile.getInterface('TestInterface')!.getIndexSignatures()[0]; + + const mockOpts = { + name: 'NamedIndexSig', // This should trigger the error + id: 'test-id', + currentPluginId: 'test-plugin', + plugins: [], + log, + captureReferences: false, + scope: ApiScope.CLIENT, + }; + + expect(() => { + buildBasicApiDeclaration(indexSig, mockOpts); + }).toThrow('Assumption is incorrect! Index signature with name: NamedIndexSig.'); + }); +}); + +describe('buildVariableDec edge cases', () => { + it('logs warning when variable has multiple call signatures', () => { + const project = new Project({ + useInMemoryFileSystem: true, + }); + + const sourceFile = project.createSourceFile( + 'test.ts', + ` + type MultiSig = { + (): string; + (x: number): number; + }; + const test: MultiSig = (() => '') as any; + ` + ); + + const varDecl = sourceFile.getVariableDeclaration('test'); + expect(varDecl).toBeDefined(); + + const warnSpy = jest.spyOn(log, 'warning').mockImplementation(); + + buildVariableDec(varDecl!, { + name: 'test', + id: 'test-id', + currentPluginId: 'test-plugin', + plugins: [], + log, + captureReferences: false, + scope: ApiScope.CLIENT, + }); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Not handling more than one call signature') + ); + + warnSpy.mockRestore(); + }); +}); + +describe('buildCallSignatureDec edge cases', () => { + it('logs warning for parameters with multiple declarations', () => { + const project = new Project({ + useInMemoryFileSystem: true, + }); + + const sourceFile = project.createSourceFile( + 'test.ts', + ` + type MultiDecl = { + (x: string): void; + }; + const test: MultiDecl = () => {}; + ` + ); + + const varDecl = sourceFile.getVariableDeclaration('test'); + expect(varDecl).toBeDefined(); + + const callSigs = varDecl!.getType().getCallSignatures(); + + if (callSigs.length > 0) { + // Create a mock signature with multiple declarations + const mockSig = { + getParameters: jest.fn(() => [ + { + getName: () => 'param', + getDeclarations: () => [varDecl, varDecl], // Multiple declarations + }, + ]), + }; + + const warnSpy = jest.spyOn(log, 'warning').mockImplementation(); + + // Cast to `any` is necessary here because we're creating a minimal mock + // of the ts-morph Signature interface to test the multiple declarations path. + buildCallSignatureDec(varDecl!, mockSig as any, { + name: 'test', + id: 'test-id', + currentPluginId: 'test-plugin', + plugins: [], + log, + captureReferences: false, + scope: ApiScope.CLIENT, + }); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Losing information on parameter') + ); + + warnSpy.mockRestore(); + } + }); +}); + +describe('getTypeKind edge cases', () => { + it('handles boolean literal types', () => { + const project = new Project({ + useInMemoryFileSystem: true, + }); + + const sourceFile = project.createSourceFile( + 'test.ts', + ` + const trueLiteral: true = true; + const falseLiteral: false = false; + ` + ); + + const trueVar = sourceFile.getVariableDeclaration('trueLiteral'); + const falseVar = sourceFile.getVariableDeclaration('falseLiteral'); + + expect(trueVar).toBeDefined(); + expect(falseVar).toBeDefined(); + + // These should be detected as BooleanKind + expect(getTypeKind(trueVar!)).toBe(TypeKind.BooleanKind); + expect(getTypeKind(falseVar!)).toBe(TypeKind.BooleanKind); + }); + + it('handles enum types', () => { + const project = new Project({ + useInMemoryFileSystem: true, + }); + + const sourceFile = project.createSourceFile( + 'test.ts', + ` + enum TestEnum { + A = 'a', + B = 'b', + } + const enumVal: TestEnum = TestEnum.A; + ` + ); + + const enumVar = sourceFile.getVariableDeclaration('enumVal'); + expect(enumVar).toBeDefined(); + + // TestEnum (not TestEnum.A) should be detected as EnumKind + expect(getTypeKind(enumVar!)).toBe(TypeKind.EnumKind); + }); +}); + +describe('getSignature edge cases', () => { + it('returns defined signature for ReactElement types', () => { + const project = new Project({ + useInMemoryFileSystem: true, + }); + + const sourceFile = project.createSourceFile( + 'test.ts', + ` + import React from 'react'; + const element: React.ReactElement =
; + ` + ); + + const varDecl = sourceFile.getVariableDeclaration('element'); + expect(varDecl).toBeDefined(); + + const signature = getSignature(varDecl!, [], log); + + // The signature should have the ReactElement hack applied if present + expect(signature).toBeDefined(); + }); +}); + +describe('getReferences edge cases', () => { + it('returns empty array for node without external references', () => { + const project = new Project({ + useInMemoryFileSystem: true, + }); + + const sourceFile = project.createSourceFile( + 'test.ts', + ` + export const testVar = 'test'; + ` + ); + + const varDecl = sourceFile.getVariableDeclaration('testVar'); + expect(varDecl).toBeDefined(); + + const testPlugin = getKibanaPlatformPlugin('pluginA'); + const testPlugins = [testPlugin]; + + // When the node has no type references to other plugins, `getReferences` should return an empty array. + const refs = getReferences({ + node: varDecl!, + plugins: testPlugins, + currentPluginId: 'pluginA', + log, + }); + + // A simple string variable has no external references. + expect(refs).toEqual([]); + }); +}); diff --git a/packages/kbn-docs-utils/src/build_api_declarations/build_basic_api_declaration.ts b/packages/kbn-docs-utils/src/build_api_declarations/build_basic_api_declaration.ts index 9712f9ac58370..d23a8ad37c977 100644 --- a/packages/kbn-docs-utils/src/build_api_declarations/build_basic_api_declaration.ts +++ b/packages/kbn-docs-utils/src/build_api_declarations/build_basic_api_declaration.ts @@ -15,7 +15,7 @@ import { getSignature } from './get_signature'; import { getTypeKind } from './get_type_kind'; import { getCommentsFromNode, getJSDocTags } from './js_doc_utils'; import type { BuildApiDecOpts } from './types'; -import { getSourceForNode } from './utils'; +import { getSourceLocationForNode } from './utils'; /** * @returns an ApiDeclaration with common functionality that every node shares. Type specific attributes, like @@ -41,6 +41,8 @@ export function buildBasicApiDeclaration(node: Node, opts: BuildApiDecOpts): Api .getText(undefined, TypeFormatFlags.OmitParameterModifiers)}`; } + const { path, lineNumber, columnNumber } = getSourceLocationForNode(node); + const apiDec = { parentPluginId: opts.currentPluginId, id: opts.id, @@ -49,7 +51,9 @@ export function buildBasicApiDeclaration(node: Node, opts: BuildApiDecOpts): Api label, description: getCommentsFromNode(node), signature: getSignature(node, opts.plugins, opts.log), - path: getSourceForNode(node), + path, + lineNumber, + columnNumber, deprecated, removeBy: removeByTag ? removeByTag.getCommentText() : undefined, trackAdoption, diff --git a/packages/kbn-docs-utils/src/build_api_declarations/extract_import_refs.test.ts b/packages/kbn-docs-utils/src/build_api_declarations/extract_import_refs.test.ts index b5ae0df352718..d57d0f0cb8aef 100644 --- a/packages/kbn-docs-utils/src/build_api_declarations/extract_import_refs.test.ts +++ b/packages/kbn-docs-utils/src/build_api_declarations/extract_import_refs.test.ts @@ -154,3 +154,17 @@ it('test single link', () => { expect(results.length).toBe(1); expect((results[0] as Reference).text).toBe('FooFoo'); }); + +it('defaults to COMMON scope for paths outside standard plugin scopes', () => { + const testPlugin = getKibanaPlatformPlugin('pluginA'); + // Path is in plugin directory but not under public/, server/, or common/. + const path = `${testPlugin.directory}/other/path.ts`; + + const results = extractImportReferences(`import("${path}").SomeType`, [testPlugin], log); + + expect(results.length).toBe(1); + const ref = results[0] as Reference; + expect(ref.text).toBe('SomeType'); + // Paths outside standard scopes should default to COMMON. + expect(ref.scope).toBe(ApiScope.COMMON); +}); 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 new file mode 100644 index 0000000000000..66c12f5ccf52b --- /dev/null +++ b/packages/kbn-docs-utils/src/build_api_declarations/js_doc_utils.test.ts @@ -0,0 +1,396 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import Path from 'path'; +import type { Node } from 'ts-morph'; +import { Project } from 'ts-morph'; +import { + getJSDocParamComment, + getJSDocReturnTagComment, + getJSDocTags, + getJSDocs, + getCommentsFromNode, +} from './js_doc_utils'; + +import { isNamedNode } from '../tsmorph_utils'; + +let project: Project; +let sourceFile: ReturnType; + +function getNodeByName(name: string): Node | undefined { + if (!sourceFile) return undefined; + + // Try function declarations first + const func = sourceFile.getFunction(name); + if (func) return func; + + // Try variable declarations + const vars = sourceFile.getVariableDeclarations(); + for (const v of vars) { + if (isNamedNode(v) && v.getName() === name) { + return v; + } + } + + return undefined; +} + +beforeAll(() => { + const tsConfigFilePath = Path.resolve( + __dirname, + '../integration_tests/__fixtures__/src/tsconfig.json' + ); + project = new Project({ + tsConfigFilePath, + }); + + sourceFile = project.getSourceFile( + Path.resolve(__dirname, '../integration_tests/__fixtures__/src/plugin_a/public/fns.ts') + ); + expect(sourceFile).toBeDefined(); +}); + +describe('getJSDocParamComment', () => { + it('extracts parameter comment for simple parameter', () => { + const node = getNodeByName('notAnArrowFn'); + expect(node).toBeDefined(); + + const comment = getJSDocParamComment(node!, 'a'); + expect(comment).toBeDefined(); + expect(comment.length).toBeGreaterThan(0); + expect(comment[0]).toContain('letter A'); + }); + + it('extracts parameter comment for multiple parameters', () => { + const node = getNodeByName('notAnArrowFn'); + expect(node).toBeDefined(); + + const commentA = getJSDocParamComment(node!, 'a'); + expect(commentA.length).toBeGreaterThan(0); + + const commentB = getJSDocParamComment(node!, 'b'); + expect(commentB.length).toBeGreaterThan(0); + expect(commentB[0]).toContain('Feed me'); + + const commentC = getJSDocParamComment(node!, 'c'); + expect(commentC.length).toBeGreaterThan(0); + expect(commentC[0]).toContain('So many params'); + }); + + it('returns empty array for parameter without JSDoc comment', () => { + const node = getNodeByName('fnWithNonExportedRef'); + expect(node).toBeDefined(); + + const comment = getJSDocParamComment(node!, 'a'); + expect(comment).toBeDefined(); + expect(comment.length).toBe(0); + }); + + it('returns empty array for non-existent parameter name', () => { + const node = getNodeByName('notAnArrowFn'); + expect(node).toBeDefined(); + + const comment = getJSDocParamComment(node!, 'nonexistent'); + expect(comment).toBeDefined(); + expect(comment.length).toBe(0); + }); + + it('handles destructured parameter parent comment', () => { + const node = getNodeByName('crazyFunction'); + expect(node).toBeDefined(); + + // Current behavior: can find parent parameter comment + const comment = getJSDocParamComment(node!, 'obj'); + expect(comment).toBeDefined(); + expect(comment.length).toBeGreaterThan(0); + expect(comment[0]).toContain('crazy parameter'); + }); + + it('does not extract property-level parameter comments (current limitation)', () => { + // This test documents current behavior: property-level @param tags like @param obj.hi + // are not currently extracted. This will be fixed in Phase 4.1. + const node = getNodeByName('crazyFunction'); + expect(node).toBeDefined(); + + // Current behavior: property-level tags are not found + const comment = getJSDocParamComment(node!, 'obj.hi'); + expect(comment).toBeDefined(); + expect(comment.length).toBe(0); // Currently returns empty, should be fixed in Phase 4.1 + + // Also test with destructured parameter name format + const comment2 = getJSDocParamComment(node!, '{ fn1, fn2 }.fn1'); + expect(comment2.length).toBe(0); // Currently returns empty + }); + + it('works with JSDoc array input', () => { + const node = getNodeByName('notAnArrowFn'); + expect(node).toBeDefined(); + + const jsDocs = getJSDocs(node!); + expect(jsDocs).toBeDefined(); + + const comment = getJSDocParamComment(jsDocs!, 'a'); + expect(comment).toBeDefined(); + expect(comment.length).toBeGreaterThan(0); + }); + + it('handles case-sensitive parameter names', () => { + const node = getNodeByName('notAnArrowFn'); + expect(node).toBeDefined(); + + // Should match exact case + const commentLower = getJSDocParamComment(node!, 'a'); + expect(commentLower.length).toBeGreaterThan(0); + + const commentUpper = getJSDocParamComment(node!, 'A'); + expect(commentUpper.length).toBe(0); // Case-sensitive, so 'A' won't match 'a' + }); +}); + +describe('getJSDocReturnTagComment', () => { + it('extracts @returns comment', () => { + const node = getNodeByName('notAnArrowFn'); + expect(node).toBeDefined(); + + const comment = getJSDocReturnTagComment(node!); + expect(comment).toBeDefined(); + expect(comment.length).toBeGreaterThan(0); + expect(comment[0]).toContain('something!'); + }); + + it('returns empty array when @returns tag is missing', () => { + const node = getNodeByName('fnWithNonExportedRef'); + expect(node).toBeDefined(); + + const comment = getJSDocReturnTagComment(node!); + expect(comment).toBeDefined(); + expect(comment.length).toBe(0); + }); + + it('works with JSDoc array input', () => { + const node = getNodeByName('notAnArrowFn'); + expect(node).toBeDefined(); + + const jsDocs = getJSDocs(node!); + expect(jsDocs).toBeDefined(); + + const comment = getJSDocReturnTagComment(jsDocs!); + expect(comment).toBeDefined(); + expect(comment.length).toBeGreaterThan(0); + }); +}); + +describe('getJSDocTags', () => { + it('extracts all JSDoc tags from node', () => { + const node = getNodeByName('notAnArrowFn'); + expect(node).toBeDefined(); + + const tags = getJSDocTags(node!); + expect(tags).toBeDefined(); + expect(tags.length).toBeGreaterThan(0); + + // Should have @param tags and @returns tag + const paramTags = tags.filter((tag) => tag.getKindName() === 'JSDocParameterTag'); + expect(paramTags.length).toBe(5); // a, b, c, d, e + + const returnTags = tags.filter((tag) => tag.getKindName() === 'JSDocReturnTag'); + expect(returnTags.length).toBe(1); + }); + + it('returns empty array when node has no JSDoc', () => { + const node = getNodeByName('fnWithNonExportedRef'); + expect(node).toBeDefined(); + + const tags = getJSDocTags(node!); + expect(tags).toBeDefined(); + expect(tags.length).toBe(0); + }); + + it('works with JSDoc array input', () => { + const node = getNodeByName('notAnArrowFn'); + expect(node).toBeDefined(); + + const jsDocs = getJSDocs(node!); + expect(jsDocs).toBeDefined(); + + const tags = getJSDocTags(jsDocs!); + expect(tags).toBeDefined(); + expect(tags.length).toBeGreaterThan(0); + }); + + it('extracts multiple JSDoc blocks if present', () => { + // Some nodes might have multiple JSDoc comments + const node = getNodeByName('notAnArrowFn'); + expect(node).toBeDefined(); + + const tags = getJSDocTags(node!); + // Should collect tags from all JSDoc blocks + expect(tags.length).toBeGreaterThan(0); + }); +}); + +describe('getJSDocs', () => { + it('extracts JSDoc from function declaration', () => { + const node = getNodeByName('notAnArrowFn'); + expect(node).toBeDefined(); + + const jsDocs = getJSDocs(node!); + expect(jsDocs).toBeDefined(); + expect(jsDocs!.length).toBeGreaterThan(0); + }); + + it('extracts JSDoc from variable declaration', () => { + const node = getNodeByName('arrowFn'); + expect(node).toBeDefined(); + + const jsDocs = getJSDocs(node!); + expect(jsDocs).toBeDefined(); + expect(jsDocs!.length).toBeGreaterThan(0); + }); + + it('returns undefined or empty array when node has no JSDoc', () => { + const node = getNodeByName('fnWithNonExportedRef'); + expect(node).toBeDefined(); + + const jsDocs = getJSDocs(node!); + // ts-morph's getJsDocs() may return empty array [] instead of undefined + expect(jsDocs === undefined || jsDocs.length === 0).toBe(true); + }); + + it('handles variable declarations with parent JSDoc', () => { + // Variable declarations might have JSDoc on the parent + const arrowFnVar = sourceFile!.getVariableDeclaration('arrowFn'); + expect(arrowFnVar).toBeDefined(); + + const jsDocs = getJSDocs(arrowFnVar!); + expect(jsDocs).toBeDefined(); + expect(jsDocs!.length).toBeGreaterThan(0); + }); + + it('handles variable declaration with grandparent JSDoc (line 39 coverage)', () => { + // Test the path where variable declaration's grandparent has JSDoc + // This covers line 38-40 in js_doc_utils.ts + const testProject = new Project({ + useInMemoryFileSystem: true, + }); + + const testSourceFile = testProject.createSourceFile( + 'test.ts', + ` + /** + * This is a JSDoc comment on the variable statement + */ + const myVar = 'test'; + ` + ); + + const varDecl = testSourceFile.getVariableDeclaration('myVar'); + expect(varDecl).toBeDefined(); + + // The variable declaration itself doesn't have JSDoc, but its grandparent (VariableStatement) does + const jsDocs = getJSDocs(varDecl!); + expect(jsDocs).toBeDefined(); + expect(jsDocs!.length).toBeGreaterThan(0); + }); +}); + +describe('getCommentsFromNode', () => { + it('extracts JSDoc description from node', () => { + const node = getNodeByName('notAnArrowFn'); + expect(node).toBeDefined(); + + const comments = getCommentsFromNode(node!); + expect(comments).toBeDefined(); + expect(comments!.length).toBeGreaterThan(0); + expect(comments![0]).toContain('non arrow function'); + }); + + it('prefers JSDoc over leading comments', () => { + const node = getNodeByName('notAnArrowFn'); + expect(node).toBeDefined(); + + const comments = getCommentsFromNode(node!); + expect(comments).toBeDefined(); + // Should use JSDoc description, not leading comment + expect(comments![0]).toContain('non arrow function'); + }); + + it('falls back to leading comments when JSDoc is missing', () => { + // Find a node without JSDoc but with leading comment + const nodesWithComments = sourceFile!.getDescendants().filter((n) => { + return n.getLeadingCommentRanges().length > 0; + }); + + if (nodesWithComments.length > 0) { + const node = nodesWithComments[0]; + const comments = getCommentsFromNode(node); + // Should extract leading comments if JSDoc is not available + expect(comments).toBeDefined(); + } + }); + + it('returns empty array when node has no comments', () => { + const node = getNodeByName('fnWithNonExportedRef'); + expect(node).toBeDefined(); + + const comments = getCommentsFromNode(node!); + // getCommentsFromNode calls getTextWithLinks which returns [] when text is empty + // The function signature says it can return undefined, but it never actually does + expect(comments).toBeDefined(); + expect(Array.isArray(comments)).toBe(true); + expect(comments!.length).toBe(0); + }); + + it('joins multiple JSDoc descriptions', () => { + // If a node has multiple JSDoc blocks, descriptions should be joined + const node = getNodeByName('notAnArrowFn'); + expect(node).toBeDefined(); + + const comments = getCommentsFromNode(node!); + expect(comments).toBeDefined(); + // Multiple JSDoc blocks would be joined with newlines + expect(comments!.length).toBeGreaterThan(0); + }); +}); + +describe('property-level JSDoc parameter tags (future enhancement)', () => { + it('currently does not support dot notation in parameter names', () => { + // This test documents the current limitation + // In Phase 4.1, we'll enhance getJSDocParamComment to support: + // - @param obj.prop + // - @param { fn1, fn2 }.fn1 + // - @param obj.nested.prop + + const node = getNodeByName('crazyFunction'); + expect(node).toBeDefined(); + + // Current behavior: dot notation is not supported + const comment1 = getJSDocParamComment(node!, 'obj.hi'); + expect(comment1.length).toBe(0); + + const comment2 = getJSDocParamComment(node!, 'obj.nested.prop'); + expect(comment2.length).toBe(0); + + // Future: should support destructured parameter names + const comment3 = getJSDocParamComment(node!, '{ fn1, fn2 }.fn1'); + expect(comment3.length).toBe(0); + }); + + it('should support nested property access patterns (future)', () => { + // This test documents expected future behavior after Phase 4.1 + // When property-level JSDoc is implemented, these should work: + // - @param obj.prop + // - @param obj.nested.prop + // - @param { destructured }.prop + // - @param { destructured }.nested.prop + + // For now, we just document that this is not yet supported + expect(true).toBe(true); // Placeholder test + }); +}); diff --git a/packages/kbn-docs-utils/src/build_api_declarations/utils.ts b/packages/kbn-docs-utils/src/build_api_declarations/utils.ts index e3a09e4e0ba07..2cace66b73201 100644 --- a/packages/kbn-docs-utils/src/build_api_declarations/utils.ts +++ b/packages/kbn-docs-utils/src/build_api_declarations/utils.ts @@ -39,6 +39,20 @@ export function getSourceForNode(node: Node): string { return path; } +export function getSourceLocationForNode(node: Node): { + path: string; + lineNumber: number; + columnNumber: number; +} { + const path = getRelativePath(node.getSourceFile().getFilePath()); + const { line, column } = node.getSourceFile().getLineAndColumnAtPos(node.getNonWhitespaceStart()); + return { + path, + lineNumber: line, + columnNumber: column, + }; +} + export function buildApiId(id: string, parentId?: string): string { const clean = id.replace(/[^A-Za-z_.$0-9]+/g, ''); return parentId ? `${parentId}.${clean}` : clean; diff --git a/packages/kbn-docs-utils/src/build_api_docs_cli.test.ts b/packages/kbn-docs-utils/src/build_api_docs_cli.test.ts new file mode 100644 index 0000000000000..cc31c96dbb7f1 --- /dev/null +++ b/packages/kbn-docs-utils/src/build_api_docs_cli.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { createFlagError } from '@kbn/dev-cli-errors'; + +// Test flag validation logic directly without executing the full CLI +describe('build_api_docs_cli flag validation', () => { + // Test the validation logic that would be in build_api_docs_cli + function isStringArray(arr: unknown | string[]): arr is string[] { + return Array.isArray(arr) && arr.every((p) => typeof p === 'string'); + } + + it('validates plugin flag must be string array', () => { + const pluginFilter = { invalid: 'object' }; + + if (pluginFilter && !isStringArray(pluginFilter)) { + expect(() => { + throw createFlagError('expected --plugin must only contain strings'); + }).toThrow('expected --plugin must only contain strings'); + } + }); + + it('validates stats flag values', () => { + const stats = ['invalid']; + + if ( + (stats && + isStringArray(stats) && + stats.find((s) => s !== 'any' && s !== 'comments' && s !== 'exports')) || + (stats && !isStringArray(stats)) + ) { + expect(() => { + throw createFlagError( + 'expected --stats must only contain `any`, `comments` and/or `exports`' + ); + }).toThrow('expected --stats must only contain'); + } + }); + + it('accepts valid stats values', () => { + const stats = ['any', 'comments']; + + if ( + (stats && + isStringArray(stats) && + stats.find((s) => s !== 'any' && s !== 'comments' && s !== 'exports')) || + (stats && !isStringArray(stats)) + ) { + throw new Error('Should not throw for valid stats'); + } + + // Should not throw + expect(stats).toEqual(['any', 'comments']); + }); + + it('handles single string plugin flag', () => { + const plugin = 'single-plugin'; + const pluginFilter = typeof plugin === 'string' ? [plugin] : plugin; + + expect(Array.isArray(pluginFilter)).toBe(true); + expect(pluginFilter).toEqual(['single-plugin']); + }); + + it('handles array plugin flag', () => { + const plugin = ['plugin1', 'plugin2']; + const pluginFilter = typeof plugin === 'string' ? [plugin] : plugin; + + expect(Array.isArray(pluginFilter)).toBe(true); + expect(pluginFilter).toEqual(['plugin1', 'plugin2']); + }); +}); diff --git a/packages/kbn-docs-utils/src/count_eslint_disable.test.ts b/packages/kbn-docs-utils/src/count_eslint_disable.test.ts index 6bd081a13f049..edb876fa5be72 100644 --- a/packages/kbn-docs-utils/src/count_eslint_disable.test.ts +++ b/packages/kbn-docs-utils/src/count_eslint_disable.test.ts @@ -34,7 +34,7 @@ describe('countEslintDisableLines', () => { expect(counts).toMatchInlineSnapshot(` Object { "eslintDisableFileCount": 3, - "eslintDisableLineCount": 9, + "eslintDisableLineCount": 10, } `); }); diff --git a/packages/kbn-docs-utils/src/find_plugins.test.ts b/packages/kbn-docs-utils/src/find_plugins.test.ts new file mode 100644 index 0000000000000..fa552324cb337 --- /dev/null +++ b/packages/kbn-docs-utils/src/find_plugins.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { findPlugins, findPackages, findTeamPlugins } from './find_plugins'; +import { ApiScope } from './types'; + +describe('findPlugins', () => { + it('returns plugins and packages when no filter is provided', () => { + const result = findPlugins(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + + // Should include @kbn/core + const core = result.find((p) => p.id === '@kbn/core'); + expect(core).toBeDefined(); + expect(core!.isPlugin).toBe(false); + }); + + it('filters plugins when pluginOrPackageFilter is provided', () => { + const result = findPlugins(['data']); + expect(Array.isArray(result)).toBe(true); + // All returned plugins should have 'data' in their ID. + result.forEach((plugin) => { + expect(plugin.id.toLowerCase()).toContain('data'); + }); + }); + + it('includes packages in the result', () => { + const result = findPlugins(); + const packages = result.filter((p) => !p.isPlugin); + expect(packages.length).toBeGreaterThan(0); + }); +}); + +describe('findPackages', () => { + it('returns all packages when no filter is provided', () => { + const result = findPackages(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + expect(result.every((p) => !p.isPlugin)).toBe(true); + }); + + it('filters packages when packageFilter is provided', () => { + const result = findPackages(['@kbn/core']); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + expect(result.every((p) => !p.isPlugin)).toBe(true); + // All returned packages should have 'core' in their ID. + result.forEach((pkg) => { + expect(pkg.id.toLowerCase()).toContain('core'); + }); + }); + + it('returns empty array when filter matches no packages', () => { + const result = findPackages(['@kbn/nonexistent-package-that-does-not-exist']); + expect(result).toEqual([]); + }); +}); + +describe('findTeamPlugins', () => { + it('returns plugins for a given team', () => { + const team = '@elastic/kibana-data-discovery'; + const result = findTeamPlugins(team); + expect(Array.isArray(result)).toBe(true); + // The function filters plugins where the team string appears in the owner array. + // We just verify it returns an array without throwing. + // Note: The owner array may contain multiple teams, so the owner name (first element) + // might not match the searched team exactly. + }); + + it('returns empty array for non-existent team', () => { + const result = findTeamPlugins('@elastic/nonexistent-team'); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(0); + }); +}); + +describe('toApiScope and toPluginOrPackage', () => { + it('correctly assigns scope for different package types', () => { + const plugins = findPlugins(); + + // Verify that plugins have correct scope assignments + plugins.forEach((plugin) => { + expect(plugin.scope).toBeDefined(); + if (plugin.scope) { + expect([ApiScope.CLIENT, ApiScope.SERVER, ApiScope.COMMON].includes(plugin.scope)).toBe( + true + ); + } + }); + }); + + it('includes required manifest fields', () => { + const plugins = findPlugins(); + + plugins.forEach((plugin) => { + expect(plugin.id).toBeDefined(); + expect(plugin.directory).toBeDefined(); + expect(plugin.manifest).toBeDefined(); + expect(plugin.manifest.id).toBeDefined(); + }); + }); +}); 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 new file mode 100644 index 0000000000000..44564608ba89f --- /dev/null +++ b/packages/kbn-docs-utils/src/get_declaration_nodes_for_plugin.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import Path from 'path'; +import { Project } from 'ts-morph'; +import { ToolingLog } from '@kbn/tooling-log'; +import { getDeclarationNodesForPluginScope } from './get_declaration_nodes_for_plugin'; +import { ApiScope } from './types'; +import type { PluginOrPackage } from './types'; + +const log = new ToolingLog({ + level: 'silent', + writeTo: process.stdout, +}); + +describe('getDeclarationNodesForPluginScope', () => { + it('handles package scope mismatch', () => { + const project = new Project({ + useInMemoryFileSystem: true, + }); + + const plugin: PluginOrPackage = { + id: 'test-package', + directory: Path.resolve(__dirname, '..'), + isPlugin: false, + scope: ApiScope.SERVER, + manifestPath: Path.resolve(__dirname, '../package.json'), + manifest: { + id: 'test-package', + owner: { name: '[Owner missing]' }, + serviceFolders: [], + }, + }; + + const nodes = getDeclarationNodesForPluginScope(project, plugin, ApiScope.CLIENT, log); + + expect(nodes).toEqual([]); + }); + + it('handles files that do not exist gracefully', () => { + const project = new Project({ + useInMemoryFileSystem: true, + }); + + const plugin: PluginOrPackage = { + id: 'test-plugin', + directory: Path.resolve(__dirname, 'nonexistent'), + isPlugin: true, + manifestPath: Path.resolve(__dirname, 'nonexistent/kibana.json'), + manifest: { + id: 'test-plugin', + pluginId: 'test-plugin', + owner: { name: '[Owner missing]' }, + serviceFolders: [], + }, + }; + + const nodes = getDeclarationNodesForPluginScope(project, plugin, ApiScope.CLIENT, log); + + // Should return empty array when file doesn't exist + expect(nodes).toEqual([]); + }); +}); diff --git a/packages/kbn-docs-utils/src/get_paths_by_package.test.ts b/packages/kbn-docs-utils/src/get_paths_by_package.test.ts new file mode 100644 index 0000000000000..cea39b84fd7d1 --- /dev/null +++ b/packages/kbn-docs-utils/src/get_paths_by_package.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { getPathsByPackage } from './get_paths_by_package'; +import { getKibanaPlatformPlugin } from './integration_tests/kibana_platform_plugin_mock'; +import type { PluginOrPackage } from './types'; + +// Mock getRepoFiles to return predictable test data +// Note: Inlining paths directly in the mock factory to avoid jest.mock restrictions +jest.mock('@kbn/get-repo-files', () => ({ + getRepoFiles: jest.fn(() => + Promise.resolve([ + { + abs: __dirname + '/integration_tests/__fixtures__/src/plugin_a/public/index.ts', + repoRel: + 'packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts', + isFixture: jest.fn(() => false), + isJavaScript: jest.fn(() => false), + isTypeScript: jest.fn(() => true), + }, + { + abs: __dirname + '/integration_tests/__fixtures__/src/plugin_a/public/fns.ts', + repoRel: + 'packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts', + isFixture: jest.fn(() => false), + isJavaScript: jest.fn(() => false), + isTypeScript: jest.fn(() => true), + }, + { + abs: __dirname + '/integration_tests/__fixtures__/src/plugin_b/public/index.ts', + repoRel: + 'packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_b/public/index.ts', + isFixture: jest.fn(() => false), + isJavaScript: jest.fn(() => false), + isTypeScript: jest.fn(() => true), + }, + { + abs: __dirname + '/integration_tests/__fixtures__/src/plugin_a/test.fixture.ts', + repoRel: + 'packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/test.fixture.ts', + isFixture: jest.fn(() => true), + isJavaScript: jest.fn(() => false), + isTypeScript: jest.fn(() => true), + }, + ]) + ), +})); + +describe('getPathsByPackage', () => { + it('groups file paths by package', async () => { + const pluginA = getKibanaPlatformPlugin('pluginA'); + const pluginB = getKibanaPlatformPlugin( + 'pluginB', + __dirname + '/integration_tests/__fixtures__/src/plugin_b' + ); + const plugins: PluginOrPackage[] = [pluginA, pluginB]; + + const pathsByPackage = await getPathsByPackage(plugins); + + expect(pathsByPackage).toBeInstanceOf(Map); + expect(pathsByPackage.size).toBeGreaterThan(0); + + // Should have paths for pluginA + const pluginAPaths = pathsByPackage.get(pluginA); + expect(pluginAPaths).toBeDefined(); + expect(Array.isArray(pluginAPaths)).toBe(true); + }); + + it('filters out fixture files', async () => { + const pluginA = getKibanaPlatformPlugin('pluginA'); + const plugins: PluginOrPackage[] = [pluginA]; + + const pathsByPackage = await getPathsByPackage(plugins); + + const pluginAPaths = pathsByPackage.get(pluginA) || []; + // Should not include .fixture.ts files + expect(pluginAPaths.every((path) => !path.includes('.fixture.'))).toBe(true); + }); + + it('filters out non-TypeScript/JavaScript files', async () => { + const pluginA = getKibanaPlatformPlugin('pluginA'); + const plugins: PluginOrPackage[] = [pluginA]; + + const pathsByPackage = await getPathsByPackage(plugins); + + const pluginAPaths = pathsByPackage.get(pluginA) || []; + // Should only include .ts or .js files + expect(pluginAPaths.every((path) => path.endsWith('.ts') || path.endsWith('.js'))).toBe(true); + }); + + it('handles empty plugin list', async () => { + const pathsByPackage = await getPathsByPackage([]); + + expect(pathsByPackage).toBeInstanceOf(Map); + expect(pathsByPackage.size).toBe(0); + }); + + it('handles packages with no matching files', async () => { + // Create a plugin that won't match any files + const emptyPlugin: PluginOrPackage = { + id: 'empty-plugin', + directory: __dirname + '/nonexistent', + isPlugin: true, + manifestPath: __dirname + '/nonexistent/kibana.json', + manifest: { + id: 'empty-plugin', + pluginId: 'empty-plugin', + owner: { name: '[Owner missing]' }, + serviceFolders: [], + }, + }; + + const pathsByPackage = await getPathsByPackage([emptyPlugin]); + + expect(pathsByPackage).toBeInstanceOf(Map); + // May or may not have entries depending on path resolution + }); +}); diff --git a/packages/kbn-docs-utils/src/get_plugin_api_map.test.ts b/packages/kbn-docs-utils/src/get_plugin_api_map.test.ts new file mode 100644 index 0000000000000..c2e037944ddf1 --- /dev/null +++ b/packages/kbn-docs-utils/src/get_plugin_api_map.test.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import Path from 'path'; +import { Project } from 'ts-morph'; +import { ToolingLog } from '@kbn/tooling-log'; +import { getPluginApiMap } from './get_plugin_api_map'; +import { getKibanaPlatformPlugin } from './integration_tests/kibana_platform_plugin_mock'; + +const log = new ToolingLog({ + level: 'silent', + writeTo: process.stdout, +}); + +describe('getPluginApiMap', () => { + let project: Project; + + beforeAll(() => { + const tsConfigFilePath = Path.resolve( + __dirname, + 'integration_tests/__fixtures__/src/tsconfig.json' + ); + project = new Project({ + tsConfigFilePath, + }); + }); + + it('builds plugin API map for multiple plugins', () => { + const pluginA = getKibanaPlatformPlugin('pluginA'); + const pluginB = getKibanaPlatformPlugin( + 'pluginB', + Path.resolve(__dirname, 'integration_tests/__fixtures__/src/plugin_b') + ); + const plugins = [pluginA, pluginB]; + + const result = getPluginApiMap(project, plugins, log, { + collectReferences: false, + }); + + expect(result.pluginApiMap).toBeDefined(); + expect(result.pluginApiMap.pluginA).toBeDefined(); + expect(result.pluginApiMap.pluginB).toBeDefined(); + expect(result.pluginApiMap.pluginA.id).toBe('pluginA'); + expect(result.pluginApiMap.pluginB.id).toBe('pluginB'); + }); + + it('collects missing API items', () => { + const pluginA = getKibanaPlatformPlugin('pluginA'); + const plugins = [pluginA]; + + const result = getPluginApiMap(project, plugins, log, { + collectReferences: false, + }); + + expect(result.missingApiItems).toBeDefined(); + // pluginA has references to non-exported types + expect(Object.keys(result.missingApiItems.pluginA || {})).toBeDefined(); + }); + + it('collects referenced deprecations', () => { + const pluginA = getKibanaPlatformPlugin('pluginA'); + const plugins = [pluginA]; + + const result = getPluginApiMap(project, plugins, log, { + collectReferences: true, + }); + + expect(result.referencedDeprecations).toBeDefined(); + expect(result.unreferencedDeprecations).toBeDefined(); + }); + + it('collects adoption tracked APIs', () => { + const pluginA = getKibanaPlatformPlugin('pluginA'); + const plugins = [pluginA]; + + const result = getPluginApiMap(project, plugins, log, { + collectReferences: false, + }); + + expect(result.adoptionTrackedAPIs).toBeDefined(); + expect(result.adoptionTrackedAPIs.pluginA).toBeDefined(); + expect(Array.isArray(result.adoptionTrackedAPIs.pluginA)).toBe(true); + }); + + it('filters plugins when pluginFilter is provided', () => { + const pluginA = getKibanaPlatformPlugin('pluginA'); + const pluginB = getKibanaPlatformPlugin( + 'pluginB', + Path.resolve(__dirname, 'integration_tests/__fixtures__/src/plugin_b') + ); + const plugins = [pluginA, pluginB]; + + const result = getPluginApiMap(project, plugins, log, { + collectReferences: false, + pluginFilter: ['pluginA'], + }); + + expect(result.pluginApiMap.pluginA).toBeDefined(); + expect(result.pluginApiMap.pluginB).toBeDefined(); + // Both plugins should be in the map, but only pluginA should capture references + // (if collectReferences is true and pluginFilter is set) + }); + + it('handles plugins with deprecated APIs', () => { + const pluginA = getKibanaPlatformPlugin('pluginA'); + const plugins = [pluginA]; + + const result = getPluginApiMap(project, plugins, log, { + collectReferences: true, + }); + + // pluginA has AnotherInterface which is deprecated + const deprecatedApis = Object.values(result.referencedDeprecations).flat(); + const unreferencedDeprecated = Object.values(result.unreferencedDeprecations).flat(); + + // Should have collected deprecations + expect(deprecatedApis.length + unreferencedDeprecated.length).toBeGreaterThanOrEqual(0); + }); + + it('handles empty plugin list', () => { + const result = getPluginApiMap(project, [], log, { + collectReferences: false, + }); + + expect(result.pluginApiMap).toEqual({}); + expect(result.missingApiItems).toEqual({}); + expect(result.referencedDeprecations).toEqual({}); + expect(result.unreferencedDeprecations).toEqual({}); + expect(result.adoptionTrackedAPIs).toEqual({}); + }); +}); diff --git a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/foo/index.ts b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/foo/index.ts index f3ffea71f68f8..34aa4f326e522 100644 --- a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/foo/index.ts +++ b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/foo/index.ts @@ -8,3 +8,9 @@ */ export const commonFoo = 'COMMON VAR!'; + +// Expected issues: +// missing comments (1): +// line 10 - commonFoo +// no references (1): +// line 10 - commonFoo diff --git a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/index.ts b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/index.ts index 150dec66007e5..aeca09816dc93 100644 --- a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/index.ts +++ b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/index.ts @@ -12,3 +12,11 @@ export { commonFoo } from './foo'; export interface ImACommonType { goo: number; } + +// Expected issues: +// missing comments (2): +// line 12 - ImACommonType +// line 13 - goo +// no references (2): +// line 12 - ImACommonType +// line 13 - goo diff --git a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts index a93bce1366694..60d80516f86d4 100644 --- a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts +++ b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts @@ -116,3 +116,39 @@ export interface ExampleInterface extends AnotherInterface { export interface IReturnAReactComponent { component: React.ComponentType; } + +// Expected issues: +// missing comments (9): +// line 32 - t +// line 41 - t +// line 44 - ExampleClass +// line 50 - component +// line 52 - Constructor +// line 52 - t +// line 71 - CrazyClass +// line 94 - foo +// line 117 - component +// no references (23): +// line 28 - WithGen +// line 32 - t +// line 41 - t +// line 44 - ExampleClass +// line 50 - component +// line 52 - Constructor +// line 52 - t +// line 56 - arrowFn +// line 60 - a +// line 62 - getVar +// line 66 - a +// line 71 - CrazyClass +// line 73 - ExampleInterface +// line 78 - getAPromiseThatResolvesToString +// line 83 - aFnWithGen +// line 89 - t +// line 91 - anOptionalFn +// line 94 - foo +// line 96 - aFn +// line 101 - fnTypeWithGeneric +// line 107 - fnTypeWithGenericThatIsOptional +// line 113 - IReturnAReactComponent +// line 117 - component diff --git a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts index 691d2b990b1dc..61046bd23a6b1 100644 --- a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts +++ b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts @@ -82,3 +82,23 @@ export const aNum = 10; * I'm a type of string, but more specifically, a literal string type. */ export const literalString = 'HI'; + +// Expected issues: +// missing comments (2): +// line 32 - a +// line 45 - foo +// no references (14): +// line 18 - aPretendNamespaceObj +// line 19 - notAnArrowFn +// line 24 - aPropertyMisdirection +// line 29 - aPropertyInlineFn +// line 32 - a +// line 36 - aPropertyStr +// line 41 - nestedObj +// line 45 - foo +// line 59 - aUnionProperty +// line 64 - aStrArray +// line 69 - aNumArray +// line 74 - aStr +// line 79 - aNum +// line 84 - literalString diff --git a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts index 15be10304fcb7..108d7998a0e0a 100644 --- a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts +++ b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts @@ -79,3 +79,74 @@ export type NotAnArrowFnType = typeof notAnArrowFn; * @internal */ export const iShouldBeInternalFn = () => 'hi'; + +// Expected issues: +// missing comments (27): +// line 24 - a +// line 24 - a +// line 24 - a +// line 25 - b +// line 25 - b +// line 25 - b +// line 26 - c +// line 26 - c +// line 26 - c +// line 27 - d +// line 27 - d +// line 27 - d +// line 28 - e +// line 28 - e +// line 28 - e +// line 66 - hi +// line 66 - obj +// line 67 - { fn1, fn2 } +// line 67 - fn1 +// line 67 - fn2 +// line 67 - foo +// line 67 - param +// line 68 - { str } +// line 68 - str +// line 74 - a +// line 74 - fnWithNonExportedRef +// line 76 - NotAnArrowFnType +// no references (40): +// line 13 - notAnArrowFn +// line 24 - a +// line 24 - a +// line 24 - a +// line 24 - a +// line 25 - b +// line 25 - b +// line 25 - b +// line 25 - b +// line 26 - c +// line 26 - c +// line 26 - c +// line 26 - c +// line 27 - d +// line 27 - d +// line 27 - d +// line 27 - d +// line 28 - e +// line 28 - e +// line 28 - e +// line 28 - e +// line 43 - arrowFn +// line 44 - a +// line 45 - b +// line 46 - c +// line 47 - d +// line 48 - e +// line 64 - crazyFunction +// line 66 - hi +// line 66 - obj +// line 67 - { fn1, fn2 } +// line 67 - fn1 +// line 67 - fn2 +// line 67 - foo +// line 67 - param +// line 68 - { str } +// line 68 - str +// line 74 - a +// line 74 - fnWithNonExportedRef +// line 76 - NotAnArrowFnType diff --git a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/foo/index.ts b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/foo/index.ts index feae01e362a5e..d8ee4920bcd7d 100644 --- a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/foo/index.ts +++ b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/foo/index.ts @@ -12,3 +12,11 @@ export const doTheFooFnThing = () => {}; export type FooType = () => 'foo'; export type ImNotExportedFromIndex = () => { bar: string }; + +// Expected issues: +// missing comments (2): +// line 10 - doTheFooFnThing +// line 12 - FooType +// no references (2): +// line 10 - doTheFooFnThing +// line 12 - FooType diff --git a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts index 4eda569bded38..336f884675976 100644 --- a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts +++ b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts @@ -33,3 +33,27 @@ export interface ClassConstructorWithStaticProperties { export function plugin() { return new PluginA(); } + +// Expected issues: +// missing comments (9): +// line 20 - imAnAny +// line 21 - imAnUnknown +// line 24 - InterfaceWithIndexSignature +// line 25 - [key: string]: { foo: string; } +// line 28 - ClassConstructorWithStaticProperties +// line 29 - staticProperty1 +// line 30 - config +// line 30 - foo +// line 30 - new +// any usage (1): +// line 20 - imAnAny +// no references (9): +// line 20 - imAnAny +// line 21 - imAnUnknown +// line 24 - InterfaceWithIndexSignature +// line 25 - [key: string]: { foo: string; } +// line 28 - ClassConstructorWithStaticProperties +// line 29 - staticProperty1 +// line 30 - config +// line 30 - foo +// line 30 - new diff --git a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts index 9b8453f20fa5b..09e6208cc75e0 100644 --- a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts +++ b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts @@ -176,3 +176,40 @@ export class PluginA implements PluginMock { return { getSearchLanguage: () => SearchLanguage.EQL, anInternalStartFn: () => 'ho' }; } } + +// Expected issues: +// missing comments (10): +// line 66 - getSearchLanguage +// line 110 - password +// line 110 - searchSpec +// line 110 - username +// line 123 - nestedVar +// line 123 - thingThree +// line 134 - obj +// line 135 - fn +// line 135 - foo +// line 135 - param +// no references (23): +// line 19 - SearchSpec +// line 24 - username +// line 28 - password +// line 52 - Start +// line 66 - getSearchLanguage +// line 77 - Setup +// line 91 - getSearchService +// line 102 - searchSpec +// line 104 - getSearchService2 +// line 110 - password +// line 110 - searchSpec +// line 110 - username +// line 112 - doTheThing +// line 123 - nestedVar +// line 123 - thingOne +// line 123 - thingThree +// line 123 - thingTwo +// line 125 - fnWithInlineParams +// line 134 - obj +// line 135 - fn +// line 135 - foo +// line 135 - param +// line 138 - id diff --git a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts index 389ba8fe9ad29..52135cda0ec76 100644 --- a/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts +++ b/packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts @@ -63,3 +63,42 @@ export interface MyProps { } export type AReactElementFn = () => ReactElement; + +// Expected issues: +// missing comments (14): +// line 19 - TypeWithGeneric +// line 21 - ImAType +// line 28 - t +// line 28 - t +// line 36 - p +// line 36 - p +// line 36 - t +// line 52 - IRefANotExportedType +// line 53 - ImAnObject +// line 54 - foo +// line 60 - MyProps +// line 61 - foo +// line 62 - bar +// line 65 - AReactElementFn +// no references (21): +// line 14 - StringOrUndefinedType +// line 19 - TypeWithGeneric +// line 21 - ImAType +// line 23 - FnWithGeneric +// line 28 - t +// line 28 - t +// line 28 - t +// line 30 - FnTypeWithGeneric +// line 36 - p +// line 36 - p +// line 36 - t +// line 36 - t +// line 38 - DayOfWeek +// line 47 - MultipleDeclarationsType +// line 52 - IRefANotExportedType +// line 53 - ImAnObject +// line 54 - foo +// line 60 - MyProps +// line 61 - foo +// line 62 - bar +// line 65 - AReactElementFn 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 907bf3d93248d..01f6224d4099c 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 @@ -38,6 +38,15 @@ let mdxOutputFolder: string; let pluginAStats: ApiStats; let pluginBStats: ApiStats; +const mapStat = ({ id, label, path, type, lineNumber, columnNumber }: ApiDeclaration) => ({ + id, + label, + path, + type, + lineNumber, + columnNumber, +}); + function linkCount(signature: TextWithLinks): number { return signature.reduce((cnt, next) => (typeof next === 'string' ? cnt : cnt + 1), 0); } @@ -138,6 +147,24 @@ beforeAll(async () => { log, }), ]); + + // Write stats snapshot alongside other generated snapshots. + const statsSnapshot = { + counts: { + apiCount: pluginAStats.apiCount, + missingExports: pluginAStats.missingExports, + missingComments: pluginAStats.missingComments.length, + isAnyType: pluginAStats.isAnyType.length, + noReferences: pluginAStats.noReferences.length, + }, + missingComments: pluginAStats.missingComments.map(mapStat), + isAnyType: pluginAStats.isAnyType.map(mapStat), + noReferences: pluginAStats.noReferences.map(mapStat), + }; + fs.writeFileSync( + Path.resolve(mdxOutputFolder, 'plugin_a.stats.json'), + JSON.stringify(statsSnapshot, null, 2) + '\n' + ); }); it('Stats', () => { @@ -651,3 +678,307 @@ describe('interfaces and classes', () => { expect(internal).toBeUndefined(); }); }); + +describe('validation and stats', () => { + describe('missing comments validation', () => { + it('validates missingComments array contains APIs without descriptions', () => { + // fnWithNonExportedRef has no JSDoc comment + const fn = doc.client.find((c) => c.label === 'fnWithNonExportedRef'); + expect(fn).toBeDefined(); + + // Check if it's in missingComments + const missingComment = pluginAStats.missingComments.find((d) => d.id === fn!.id); + expect(missingComment).toBeDefined(); + }); + + it('validates missingComments includes destructured parameter children', () => { + // crazyFunction has destructured params with nested properties + // Current behavior: nested properties without comments are flagged + const fn = doc.client.find((c) => c.label === 'crazyFunction'); + expect(fn).toBeDefined(); + + const objParam = fn!.children?.find((c) => c.label === 'obj'); + expect(objParam).toBeDefined(); + + const hiProp = objParam!.children?.find((c) => c.label === 'hi'); + expect(hiProp).toBeDefined(); + + // Current behavior: property without comment is flagged + // Note: This is a false positive that will be fixed in Phase 4.2 + // Verify the property structure exists and check if it's in missingComments + expect(hiProp!.description).toBeDefined(); + const hasDescription = hiProp!.description!.length > 0; + const missingComment = pluginAStats.missingComments.find((d) => d.id === hiProp!.id); + + // If property has no description, it should be in missingComments + // If it has a description, it should not be in missingComments + if (!hasDescription) { + expect(missingComment).toBeDefined(); + } else { + expect(missingComment).toBeUndefined(); + } + }); + + it('validates missingComments does not include APIs with descriptions', () => { + // notAnArrowFn has complete JSDoc + const fn = doc.client.find((c) => c.label === 'notAnArrowFn'); + expect(fn).toBeDefined(); + + const missingComment = pluginAStats.missingComments.find((d) => d.id === fn!.id); + expect(missingComment).toBeUndefined(); + }); + + it('validates missingComments includes type aliases without documentation', () => { + // NotAnArrowFnType has no JSDoc comment + const type = doc.client.find((c) => c.label === 'NotAnArrowFnType'); + expect(type).toBeDefined(); + + const missingComment = pluginAStats.missingComments.find((d) => d.id === type!.id); + expect(missingComment).toBeDefined(); + }); + }); + + describe('destructured parameter validation', () => { + it('validates destructured parameter structure is extracted correctly', () => { + const fn = doc.client.find((c) => c.label === 'crazyFunction'); + expect(fn).toBeDefined(); + + // First parameter: obj: { hi: string } + const objParam = fn!.children?.find((c) => c.label === 'obj'); + expect(objParam).toBeDefined(); + expect(objParam!.children).toBeDefined(); + expect(objParam!.children!.length).toBe(1); + + // Second parameter: { fn1, fn2 } + const fnParam = fn!.children?.find((c) => c.label === '{ fn1, fn2 }'); + expect(fnParam).toBeDefined(); + expect(fnParam!.children).toBeDefined(); + expect(fnParam!.children!.length).toBe(2); + }); + + it('validates parent parameter comment is extracted for destructured params', () => { + const fn = doc.client.find((c) => c.label === 'crazyFunction'); + expect(fn).toBeDefined(); + + const objParam = fn!.children?.find((c) => c.label === 'obj'); + expect(objParam).toBeDefined(); + + // Current behavior: parent parameter comments are NOT extracted for TypeLiteral parameters + // This is a known limitation - when a parameter has a TypeLiteral type (destructured params), + // buildApiDeclaration is called directly without extracting the JSDoc comment for the parameter name. + // This will be fixed in Phase 4.1 + expect(objParam!.description).toBeDefined(); + // Currently, the description is empty for destructured parameters + // After Phase 4.1, this should contain the @param obj comment + expect(objParam!.description!.length).toBe(0); + }); + + it('validates property-level comments are not currently extracted (current limitation)', () => { + // This test documents current behavior: property-level @param tags are not extracted + // After Phase 4.1, this should be updated to test that property-level comments ARE extracted + const fn = doc.client.find((c) => c.label === 'crazyFunction'); + expect(fn).toBeDefined(); + + const objParam = fn!.children?.find((c) => c.label === 'obj'); + expect(objParam).toBeDefined(); + + const hiProp = objParam!.children?.find((c) => c.label === 'hi'); + expect(hiProp).toBeDefined(); + + // Current behavior: property-level comments are not extracted + // Even if @param obj.hi existed in JSDoc, it wouldn't be found + expect(hiProp!.description).toBeDefined(); + expect(hiProp!.description!.length).toBe(0); + }); + + it('validates nested destructured parameters are extracted', () => { + const fn = doc.client.find((c) => c.label === 'crazyFunction'); + expect(fn).toBeDefined(); + + const fnParam = fn!.children?.find((c) => c.label === '{ fn1, fn2 }'); + expect(fnParam).toBeDefined(); + + const fn1 = fnParam!.children?.find((c) => c.label === 'fn1'); + expect(fn1).toBeDefined(); + expect(fn1!.type).toBe(TypeKind.FunctionKind); + expect(fn1!.children).toBeDefined(); + + // fn1 has nested parameter: foo: { param: string } + const fooParam = fn1!.children?.find((c) => c.label === 'foo'); + expect(fooParam).toBeDefined(); + expect(fooParam!.type).toBe(TypeKind.ObjectKind); + expect(fooParam!.children).toBeDefined(); + + const paramProp = fooParam!.children?.find((c) => c.label === 'param'); + expect(paramProp).toBeDefined(); + expect(paramProp!.type).toBe(TypeKind.StringKind); + }); + }); + + describe('inline object parameter validation', () => { + it('validates inline object parameters are extracted as children', () => { + const grouped = groupPluginApi(doc.client); + const setup = grouped.setup; + expect(setup).toBeDefined(); + + const getSearchService2 = setup!.children?.find((c) => c.label === 'getSearchService2'); + expect(getSearchService2).toBeDefined(); + expect(getSearchService2!.type).toBe(TypeKind.FunctionKind); + expect(getSearchService2!.children).toBeDefined(); + + const searchSpecParam = getSearchService2!.children?.find((c) => c.label === 'searchSpec'); + expect(searchSpecParam).toBeDefined(); + expect(searchSpecParam!.children).toBeDefined(); + expect(searchSpecParam!.children!.length).toBe(2); // username and password + }); + + it('validates missing property-level comments on inline object parameters', () => { + const grouped = groupPluginApi(doc.client); + const setup = grouped.setup; + expect(setup).toBeDefined(); + + const getSearchService2 = setup!.children?.find((c) => c.label === 'getSearchService2'); + expect(getSearchService2).toBeDefined(); + + const searchSpecParam = getSearchService2!.children?.find((c) => c.label === 'searchSpec'); + expect(searchSpecParam).toBeDefined(); + + const usernameProp = searchSpecParam!.children?.find((c) => c.label === 'username'); + expect(usernameProp).toBeDefined(); + + // Current behavior: property without comment is flagged + // Note: This is a false positive that will be fixed in Phase 4.2 + // Verify the property structure exists and check if it's in missingComments + expect(usernameProp!.description).toBeDefined(); + const hasDescription = usernameProp!.description!.length > 0; + const missingComment = pluginAStats.missingComments.find((d) => d.id === usernameProp!.id); + + // If property has no description, it should be in missingComments + // If it has a description, it should not be in missingComments + if (!hasDescription) { + expect(missingComment).toBeDefined(); + } else { + expect(missingComment).toBeUndefined(); + } + }); + + it('validates deeply nested inline object parameters', () => { + const grouped = groupPluginApi(doc.client); + const setup = grouped.setup; + expect(setup).toBeDefined(); + + const fnWithInlineParams = setup!.children?.find((c) => c.label === 'fnWithInlineParams'); + expect(fnWithInlineParams).toBeDefined(); + + const objParam = fnWithInlineParams!.children?.find((c) => c.label === 'obj'); + expect(objParam).toBeDefined(); + + const fnProp = objParam!.children?.find((c) => c.label === 'fn'); + expect(fnProp).toBeDefined(); + expect(fnProp!.type).toBe(TypeKind.FunctionKind); + + const fooParam = fnProp!.children?.find((c) => c.label === 'foo'); + expect(fooParam).toBeDefined(); + expect(fooParam!.type).toBe(TypeKind.ObjectKind); + + const paramProp = fooParam!.children?.find((c) => c.label === 'param'); + expect(paramProp).toBeDefined(); + expect(paramProp!.type).toBe(TypeKind.StringKind); + }); + }); + + describe('stats validation output', () => { + it('validates stats structure contains all required fields', () => { + expect(pluginAStats).toBeDefined(); + expect(pluginAStats.missingComments).toBeDefined(); + expect(Array.isArray(pluginAStats.missingComments)).toBe(true); + expect(pluginAStats.isAnyType).toBeDefined(); + expect(Array.isArray(pluginAStats.isAnyType)).toBe(true); + expect(pluginAStats.noReferences).toBeDefined(); + expect(Array.isArray(pluginAStats.noReferences)).toBe(true); + expect(typeof pluginAStats.apiCount).toBe('number'); + expect(typeof pluginAStats.missingExports).toBe('number'); + expect(typeof pluginAStats.deprecatedAPIsReferencedCount).toBe('number'); + expect(typeof pluginAStats.adoptionTrackedAPIsCount).toBe('number'); + }); + + it('validates isAnyType array contains correct APIs', () => { + expect(pluginAStats.isAnyType.length).toBe(1); + const anyType = pluginAStats.isAnyType[0]; + expect(anyType).toBeDefined(); + expect(anyType.id).toContain('imAnAny'); + }); + + it('validates missingComments array contains ApiDeclaration objects with required fields', () => { + if (pluginAStats.missingComments.length > 0) { + const missing = pluginAStats.missingComments[0]; + expect(missing).toBeDefined(); + expect(missing.id).toBeDefined(); + expect(missing.label).toBeDefined(); + expect(missing.path).toBeDefined(); + expect(missing.type).toBeDefined(); + expect(missing.parentPluginId).toBeDefined(); + } + }); + + it('validates noReferences array structure', () => { + expect(Array.isArray(pluginAStats.noReferences)).toBe(true); + if (pluginAStats.noReferences.length > 0) { + const noRef = pluginAStats.noReferences[0]; + expect(noRef).toBeDefined(); + expect(noRef.id).toBeDefined(); + // APIs with no references should have undefined or empty references array + expect(noRef.references === undefined || noRef.references.length === 0).toBe(true); + } + }); + + it('validates apiCount matches actual API declarations', () => { + const totalApis = doc.client.length + doc.server.length + doc.common.length; + // apiCount includes children, so it should be >= total top-level APIs + expect(pluginAStats.apiCount).toBeGreaterThanOrEqual(totalApis); + }); + + it('captures stats snapshot', () => { + // Stats snapshot is written in beforeAll alongside other snapshots. + // This test verifies the snapshot file was created and matches. + const snapshotPath = Path.resolve(__dirname, 'snapshots', 'plugin_a.stats.json'); + const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf8')); + + expect(snapshot.counts.apiCount).toBe(pluginAStats.apiCount); + expect(snapshot.counts.missingComments).toBe(pluginAStats.missingComments.length); + expect(snapshot.counts.isAnyType).toBe(pluginAStats.isAnyType.length); + expect(snapshot.counts.noReferences).toBe(pluginAStats.noReferences.length); + }); + }); + + describe('property-level JSDoc validation (future enhancement)', () => { + it('documents that property-level JSDoc is not currently validated', () => { + // This test documents current limitation + // After Phase 4.1, property-level @param tags like @param obj.prop should be validated + const fn = doc.client.find((c) => c.label === 'crazyFunction'); + expect(fn).toBeDefined(); + + const objParam = fn!.children?.find((c) => c.label === 'obj'); + expect(objParam).toBeDefined(); + + const hiProp = objParam!.children?.find((c) => c.label === 'hi'); + expect(hiProp).toBeDefined(); + + // Current behavior: property-level comments are not checked + // Future: should check for @param obj.hi and not flag as missing if it exists + const missingComment = pluginAStats.missingComments.find((d) => d.id === hiProp!.id); + // Currently flagged as missing (false positive) + expect(missingComment).toBeDefined(); + }); + + it('documents expected future behavior for property-level validation', () => { + // After Phase 4.1 and 4.2, the validation should: + // 1. Check for property-level @param tags (e.g., @param obj.prop) + // 2. Not flag as missing if parent has comment OR property-level tag exists + // 3. Support nested property access (e.g., @param obj.nested.prop) + + // For now, this is a placeholder test documenting expected behavior + expect(true).toBe(true); + }); + }); +}); diff --git a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.devdocs.json b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.devdocs.json index 34f9b61a99886..666d308b99ef2 100644 --- a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.devdocs.json +++ b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.devdocs.json @@ -36,6 +36,8 @@ "

>" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 71, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "children": [], @@ -67,6 +69,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 44, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "children": [ @@ -81,6 +85,8 @@ "React.ComponentType<{}> | undefined" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 50, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -95,6 +101,8 @@ "any" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 52, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "children": [ @@ -109,6 +117,8 @@ "T" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 52, + "columnNumber": 15, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -144,6 +154,8 @@ } ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 56, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "children": [ @@ -166,6 +178,8 @@ } ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 60, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -194,6 +208,8 @@ ") => string" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 62, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "children": [ @@ -216,6 +232,8 @@ } ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 66, + "columnNumber": 10, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -265,6 +283,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 43, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "children": [ @@ -281,6 +301,8 @@ "string" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 44, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -298,6 +320,8 @@ "number | undefined" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 45, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "isRequired": false @@ -322,6 +346,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 46, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -345,6 +371,8 @@ } ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 47, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -362,6 +390,8 @@ "string | undefined" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 48, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "isRequired": false @@ -385,6 +415,8 @@ "(obj: { hi: string; }, { fn1, fn2 }: { fn1: (foo: { param: string; }) => number; fn2: () => void; }, { str }: { str: string; }) => () => () => number" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 64, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "children": [ @@ -396,6 +428,8 @@ "label": "obj", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 66, + "columnNumber": 10, "deprecated": false, "trackAdoption": false, "children": [ @@ -407,6 +441,8 @@ "label": "hi", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 66, + "columnNumber": 12, "deprecated": false, "trackAdoption": false } @@ -420,6 +456,8 @@ "label": "{ fn1, fn2 }", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 67, + "columnNumber": 19, "deprecated": false, "trackAdoption": false, "children": [ @@ -434,6 +472,8 @@ "(foo: { param: string; }) => number" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 67, + "columnNumber": 21, "deprecated": false, "trackAdoption": false, "children": [ @@ -445,6 +485,8 @@ "label": "foo", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 67, + "columnNumber": 32, "deprecated": false, "trackAdoption": false, "children": [ @@ -456,6 +498,8 @@ "label": "param", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 67, + "columnNumber": 34, "deprecated": false, "trackAdoption": false } @@ -475,6 +519,8 @@ "() => void" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 67, + "columnNumber": 62, "deprecated": false, "trackAdoption": false, "children": [], @@ -490,6 +536,8 @@ "label": "{ str }", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 68, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "children": [ @@ -501,6 +549,8 @@ "label": "str", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 68, + "columnNumber": 16, "deprecated": false, "trackAdoption": false } @@ -526,6 +576,8 @@ "ImNotExportedFromIndex" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 74, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "children": [ @@ -540,6 +592,8 @@ "ImNotExportedFromIndex" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 74, + "columnNumber": 38, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -585,6 +639,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 13, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "children": [ @@ -601,6 +657,8 @@ "string" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 24, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -618,6 +676,8 @@ "number | undefined" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 25, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "isRequired": false @@ -642,6 +702,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 26, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -665,6 +727,8 @@ } ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 27, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -682,6 +746,8 @@ "string | undefined" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 28, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "isRequired": false @@ -716,6 +782,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 35, + "columnNumber": 1, "deprecated": true, "removeBy": "8.0", "trackAdoption": false, @@ -741,6 +809,8 @@ "T" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 41, + "columnNumber": 3, "deprecated": false, "trackAdoption": false } @@ -755,6 +825,8 @@ "label": "ClassConstructorWithStaticProperties", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "lineNumber": 28, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "children": [ @@ -766,6 +838,8 @@ "label": "staticProperty1", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "lineNumber": 29, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -780,6 +854,8 @@ "any" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "lineNumber": 30, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "children": [ @@ -791,6 +867,8 @@ "label": "config", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "lineNumber": 30, + "columnNumber": 16, "deprecated": false, "trackAdoption": false, "children": [ @@ -802,6 +880,8 @@ "label": "foo", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "lineNumber": 30, + "columnNumber": 18, "deprecated": false, "trackAdoption": false } @@ -841,6 +921,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 73, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "children": [ @@ -857,6 +939,8 @@ "() => Promise" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 78, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "children": [], @@ -875,6 +959,8 @@ "(t: T) => void" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 83, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "children": [ @@ -891,6 +977,8 @@ "T" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 89, + "columnNumber": 19, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -913,6 +1001,8 @@ "((foo: string) => string) | undefined" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 91, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "children": [ @@ -927,6 +1017,8 @@ "string" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 94, + "columnNumber": 19, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -947,6 +1039,8 @@ "() => void" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 96, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "children": [], @@ -981,6 +1075,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 101, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "returnComment": [], @@ -996,6 +1092,8 @@ "T" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 36, + "columnNumber": 37, "deprecated": false, "trackAdoption": false }, @@ -1016,6 +1114,8 @@ } ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 36, + "columnNumber": 43, "deprecated": false, "trackAdoption": false } @@ -1041,6 +1141,8 @@ " | undefined" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 107, + "columnNumber": 3, "deprecated": false, "trackAdoption": false } @@ -1055,6 +1157,8 @@ "label": "ImAnObject", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 53, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "children": [ @@ -1077,6 +1181,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 54, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "returnComment": [], @@ -1092,6 +1198,8 @@ "T" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 28, + "columnNumber": 33, "deprecated": false, "trackAdoption": false } @@ -1108,6 +1216,8 @@ "label": "InterfaceWithIndexSignature", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "lineNumber": 24, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "children": [ @@ -1122,6 +1232,8 @@ "[key: string]: { foo: string; }" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "lineNumber": 25, + "columnNumber": 3, "deprecated": false, "trackAdoption": false } @@ -1138,6 +1250,8 @@ "\nAn interface that has a react component." ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 113, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "children": [ @@ -1152,6 +1266,8 @@ "React.ComponentClass<{}, any> | React.FunctionComponent<{}>" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 117, + "columnNumber": 3, "deprecated": false, "trackAdoption": false } @@ -1166,6 +1282,8 @@ "label": "MyProps", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 60, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "children": [ @@ -1177,6 +1295,8 @@ "label": "foo", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 61, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -1199,6 +1319,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 62, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "returnComment": [], @@ -1214,6 +1336,8 @@ "T" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 28, + "columnNumber": 33, "deprecated": false, "trackAdoption": false } @@ -1232,6 +1356,8 @@ "\nThe SearchSpec interface contains settings for creating a new SearchService, like\nusername and password." ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 19, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "children": [ @@ -1245,6 +1371,8 @@ "\nStores the username. Duh," ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 24, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -1258,6 +1386,8 @@ "\nStores the password. I hope it's encrypted!" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 28, + "columnNumber": 3, "deprecated": false, "trackAdoption": false } @@ -1284,6 +1414,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 28, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "children": [ @@ -1298,6 +1430,8 @@ "T" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 32, + "columnNumber": 3, "deprecated": false, "trackAdoption": false } @@ -1316,6 +1450,8 @@ "\nComments on enums." ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 38, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1335,6 +1471,8 @@ "10" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 79, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1352,6 +1490,8 @@ "number[]" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 69, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1375,6 +1515,8 @@ ", string | React.JSXElementConstructor>" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 65, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "returnComment": [], @@ -1391,6 +1533,8 @@ "\nA string that says hi to you!" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 74, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1408,6 +1552,8 @@ "string[]" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 64, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1433,6 +1579,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 59, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1468,6 +1616,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 30, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "returnComment": [ @@ -1487,6 +1637,8 @@ "T" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 36, + "columnNumber": 37, "deprecated": false, "trackAdoption": false }, @@ -1507,6 +1659,8 @@ } ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 36, + "columnNumber": 43, "deprecated": false, "trackAdoption": false } @@ -1534,6 +1688,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 23, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "returnComment": [], @@ -1551,6 +1707,8 @@ "T" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 28, + "columnNumber": 33, "deprecated": false, "trackAdoption": false } @@ -1568,6 +1726,8 @@ "any" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "lineNumber": 20, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1583,6 +1743,8 @@ "unknown" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "lineNumber": 21, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1621,6 +1783,8 @@ } ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 21, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1637,6 +1801,8 @@ " | { zed: \"hi\"; }" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 52, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1654,6 +1820,8 @@ "\"HI\"" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 84, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1679,6 +1847,8 @@ "[]" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 47, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1718,6 +1888,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 76, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "returnComment": [], @@ -1730,6 +1902,8 @@ "label": "a", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 24, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -1744,6 +1918,8 @@ "number | undefined" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 25, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -1758,6 +1934,8 @@ "string[]" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 26, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -1795,6 +1973,8 @@ } ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 27, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -1809,6 +1989,8 @@ "string | undefined" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 28, + "columnNumber": 3, "deprecated": false, "trackAdoption": false } @@ -1828,6 +2010,8 @@ "string | undefined" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 14, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1843,6 +2027,8 @@ "T[]" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 19, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -1859,6 +2045,8 @@ "\nSome of the plugins wrap static exports in an object to create\na namespace like this." ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 18, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "children": [ @@ -1899,6 +2087,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 19, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "returnComment": [], @@ -1911,6 +2101,8 @@ "label": "a", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 24, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -1925,6 +2117,8 @@ "number | undefined" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 25, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -1939,6 +2133,8 @@ "string[]" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 26, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -1976,6 +2172,8 @@ } ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 27, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -1990,6 +2188,8 @@ "string | undefined" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 28, + "columnNumber": 3, "deprecated": false, "trackAdoption": false } @@ -2032,6 +2232,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 24, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "returnComment": [], @@ -2044,6 +2246,8 @@ "label": "a", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 24, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -2058,6 +2262,8 @@ "number | undefined" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 25, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -2072,6 +2278,8 @@ "string[]" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 26, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -2109,6 +2317,8 @@ } ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 27, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -2123,6 +2333,8 @@ "string | undefined" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 28, + "columnNumber": 3, "deprecated": false, "trackAdoption": false } @@ -2156,6 +2368,8 @@ } ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 29, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "children": [ @@ -2176,6 +2390,8 @@ } ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 32, + "columnNumber": 23, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -2193,6 +2409,8 @@ "/**\n * The only way for this to have a comment is to grab this.\n */" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 36, + "columnNumber": 3, "deprecated": false, "trackAdoption": false }, @@ -2206,6 +2424,8 @@ "/**\n * Will this nested object have it's children extracted appropriately?\n */" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 41, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "children": [ @@ -2217,6 +2437,8 @@ "label": "foo", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 45, + "columnNumber": 5, "deprecated": false, "trackAdoption": false } @@ -2236,6 +2458,8 @@ "\nAccess setup functionality from your plugin's setup function by adding the example\nplugin as a dependency.\n\n```ts\nClass MyPlugin {\n setup(core: CoreDependencies, { example }: PluginDependencies) {\n // Here you can access this functionality.\n example.getSearchService();\n }\n}\n```" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 77, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "children": [ @@ -2262,6 +2486,8 @@ ") => string" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 91, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "children": [ @@ -2284,6 +2510,8 @@ } ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 102, + "columnNumber": 22, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -2306,6 +2534,8 @@ "(searchSpec: { username: string; password: string; }) => string" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 104, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "children": [ @@ -2317,6 +2547,8 @@ "label": "searchSpec", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 110, + "columnNumber": 35, "deprecated": false, "trackAdoption": false, "children": [ @@ -2328,6 +2560,8 @@ "label": "username", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 110, + "columnNumber": 37, "deprecated": false, "trackAdoption": false }, @@ -2339,6 +2573,8 @@ "label": "password", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 110, + "columnNumber": 55, "deprecated": false, "trackAdoption": false } @@ -2362,6 +2598,8 @@ "(thingOne: number, thingTwo: string, thingThree: { nestedVar: number; }) => void" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 112, + "columnNumber": 3, "deprecated": true, "trackAdoption": false, "references": [], @@ -2379,6 +2617,8 @@ "number" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 123, + "columnNumber": 16, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -2396,6 +2636,8 @@ "string" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 123, + "columnNumber": 34, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -2408,6 +2650,8 @@ "label": "thingThree", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 123, + "columnNumber": 64, "deprecated": false, "trackAdoption": false, "children": [ @@ -2419,6 +2663,8 @@ "label": "nestedVar", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 123, + "columnNumber": 66, "deprecated": false, "trackAdoption": false } @@ -2440,6 +2686,8 @@ "(obj: { fn: (foo: { param: string; }) => number; }) => () => { retFoo: () => string; }" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 125, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "children": [ @@ -2451,6 +2699,8 @@ "label": "obj", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 134, + "columnNumber": 29, "deprecated": false, "trackAdoption": false, "children": [ @@ -2465,6 +2715,8 @@ "(foo: { param: string; }) => number" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 135, + "columnNumber": 5, "deprecated": false, "trackAdoption": false, "children": [ @@ -2476,6 +2728,8 @@ "label": "foo", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 135, + "columnNumber": 15, "deprecated": false, "trackAdoption": false, "children": [ @@ -2487,6 +2741,8 @@ "label": "param", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 135, + "columnNumber": 17, "deprecated": false, "trackAdoption": false } @@ -2512,6 +2768,8 @@ "\nHi, I'm a comment for an id string!" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 138, + "columnNumber": 3, "deprecated": false, "trackAdoption": false } @@ -2529,6 +2787,8 @@ "\nAccess start functionality from your plugin's start function by adding the example\nplugin as a dependency.\n\n```ts\nClass MyPlugin {\n start(core: CoreDependencies, { example }: PluginDependencies) {\n // Here you can access this functionality.\n example.getSearchLanguage();\n }\n}\n```" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 52, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "children": [ @@ -2544,6 +2804,8 @@ "SearchLanguage" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 66, + "columnNumber": 3, "deprecated": false, "trackAdoption": false, "children": [], @@ -2576,6 +2838,8 @@ "label": "ImACommonType", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/index.ts", + "lineNumber": 12, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "children": [ @@ -2587,6 +2851,8 @@ "label": "goo", "description": [], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/index.ts", + "lineNumber": 13, + "columnNumber": 3, "deprecated": false, "trackAdoption": false } @@ -2598,4 +2864,4 @@ "misc": [], "objects": [] } -} +} \ No newline at end of file diff --git a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.mdx b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.mdx index 5a7bc515413cc..22eba0f9b8c5d 100644 --- a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.mdx +++ b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/pluginA title: "pluginA" image: https://source.unsplash.com/400x175/?github description: API docs for the pluginA plugin -date: 2022-09-05 +date: 2025-12-31 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginA'] --- import pluginAObj from './plugin_a.devdocs.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 new file mode 100644 index 0000000000000..8aea98ab33e4b --- /dev/null +++ b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.stats.json @@ -0,0 +1,1711 @@ +{ + "counts": { + "apiCount": 136, + "missingExports": 2, + "missingComments": 76, + "isAnyType": 1, + "noReferences": 135 + }, + "missingComments": [ + { + "id": "def-public.Setup.getSearchService2.$1", + "label": "searchSpec", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Object", + "lineNumber": 110, + "columnNumber": 35 + }, + { + "id": "def-public.Setup.getSearchService2.$1.username", + "label": "username", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "string", + "lineNumber": 110, + "columnNumber": 37 + }, + { + "id": "def-public.Setup.getSearchService2.$1.password", + "label": "password", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "string", + "lineNumber": 110, + "columnNumber": 55 + }, + { + "id": "def-public.Setup.doTheThing.$3", + "label": "thingThree", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Object", + "lineNumber": 123, + "columnNumber": 64 + }, + { + "id": "def-public.Setup.doTheThing.$3.nestedVar", + "label": "nestedVar", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "number", + "lineNumber": 123, + "columnNumber": 66 + }, + { + "id": "def-public.Setup.fnWithInlineParams.$1", + "label": "obj", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Object", + "lineNumber": 134, + "columnNumber": 29 + }, + { + "id": "def-public.Setup.fnWithInlineParams.$1.fn", + "label": "fn", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Function", + "lineNumber": 135, + "columnNumber": 5 + }, + { + "id": "def-public.Setup.fnWithInlineParams.$1.fn.$1", + "label": "foo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Object", + "lineNumber": 135, + "columnNumber": 15 + }, + { + "id": "def-public.Setup.fnWithInlineParams.$1.fn.$1.param", + "label": "param", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "string", + "lineNumber": 135, + "columnNumber": 17 + }, + { + "id": "def-public.Start.getSearchLanguage", + "label": "getSearchLanguage", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Function", + "lineNumber": 66, + "columnNumber": 3 + }, + { + "id": "def-public.doTheFooFnThing", + "label": "doTheFooFnThing", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/foo/index.ts", + "type": "Function", + "lineNumber": 10, + "columnNumber": 14 + }, + { + "id": "def-public.FooType", + "label": "FooType", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/foo/index.ts", + "type": "Type", + "lineNumber": 12, + "columnNumber": 1 + }, + { + "id": "def-public.imAnAny", + "label": "imAnAny", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "Any", + "lineNumber": 20, + "columnNumber": 14 + }, + { + "id": "def-public.imAnUnknown", + "label": "imAnUnknown", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "Unknown", + "lineNumber": 21, + "columnNumber": 14 + }, + { + "id": "def-public.InterfaceWithIndexSignature", + "label": "InterfaceWithIndexSignature", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "Interface", + "lineNumber": 24, + "columnNumber": 1 + }, + { + "id": "def-public.InterfaceWithIndexSignature.Unnamed", + "label": "[key: string]: { foo: string; }", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "IndexSignature", + "lineNumber": 25, + "columnNumber": 3 + }, + { + "id": "def-public.ClassConstructorWithStaticProperties", + "label": "ClassConstructorWithStaticProperties", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "Interface", + "lineNumber": 28, + "columnNumber": 1 + }, + { + "id": "def-public.ClassConstructorWithStaticProperties.staticProperty1", + "label": "staticProperty1", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "string", + "lineNumber": 29, + "columnNumber": 3 + }, + { + "id": "def-public.ClassConstructorWithStaticProperties.new", + "label": "new", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "Function", + "lineNumber": 30, + "columnNumber": 3 + }, + { + "id": "def-public.ClassConstructorWithStaticProperties.new.$1", + "label": "config", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "Object", + "lineNumber": 30, + "columnNumber": 16 + }, + { + "id": "def-public.ClassConstructorWithStaticProperties.new.$1.foo", + "label": "foo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "string", + "lineNumber": 30, + "columnNumber": 18 + }, + { + "id": "def-public.crazyFunction.$1", + "label": "obj", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Object", + "lineNumber": 66, + "columnNumber": 10 + }, + { + "id": "def-public.crazyFunction.$1.hi", + "label": "hi", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 66, + "columnNumber": 12 + }, + { + "id": "def-public.crazyFunction.$2", + "label": "{ fn1, fn2 }", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Object", + "lineNumber": 67, + "columnNumber": 19 + }, + { + "id": "def-public.crazyFunction.$2.fn1", + "label": "fn1", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Function", + "lineNumber": 67, + "columnNumber": 21 + }, + { + "id": "def-public.crazyFunction.$2.fn1.$1", + "label": "foo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Object", + "lineNumber": 67, + "columnNumber": 32 + }, + { + "id": "def-public.crazyFunction.$2.fn1.$1.param", + "label": "param", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 67, + "columnNumber": 34 + }, + { + "id": "def-public.crazyFunction.$2.fn2", + "label": "fn2", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Function", + "lineNumber": 67, + "columnNumber": 62 + }, + { + "id": "def-public.crazyFunction.$3", + "label": "{ str }", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Object", + "lineNumber": 68, + "columnNumber": 14 + }, + { + "id": "def-public.crazyFunction.$3.str", + "label": "str", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 68, + "columnNumber": 16 + }, + { + "id": "def-public.fnWithNonExportedRef", + "label": "fnWithNonExportedRef", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Function", + "lineNumber": 74, + "columnNumber": 14 + }, + { + "id": "def-public.fnWithNonExportedRef.$1", + "label": "a", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Function", + "lineNumber": 74, + "columnNumber": 38 + }, + { + "id": "def-public.NotAnArrowFnType", + "label": "NotAnArrowFnType", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Type", + "lineNumber": 76, + "columnNumber": 1 + }, + { + "id": "def-public.NotAnArrowFnType.$1", + "label": "a", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 24, + "columnNumber": 3 + }, + { + "id": "def-public.NotAnArrowFnType.$2", + "label": "b", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "number", + "lineNumber": 25, + "columnNumber": 3 + }, + { + "id": "def-public.NotAnArrowFnType.$3", + "label": "c", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Array", + "lineNumber": 26, + "columnNumber": 3 + }, + { + "id": "def-public.NotAnArrowFnType.$4", + "label": "d", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "CompoundType", + "lineNumber": 27, + "columnNumber": 3 + }, + { + "id": "def-public.NotAnArrowFnType.$5", + "label": "e", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 28, + "columnNumber": 3 + }, + { + "id": "def-public.WithGen.t", + "label": "t", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Uncategorized", + "lineNumber": 32, + "columnNumber": 3 + }, + { + "id": "def-public.AnotherInterface.t", + "label": "t", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Uncategorized", + "lineNumber": 41, + "columnNumber": 3 + }, + { + "id": "def-public.ExampleClass", + "label": "ExampleClass", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Class", + "lineNumber": 44, + "columnNumber": 1 + }, + { + "id": "def-public.ExampleClass.component", + "label": "component", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "CompoundType", + "lineNumber": 50, + "columnNumber": 3 + }, + { + "id": "def-public.ExampleClass.Unnamed", + "label": "Constructor", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Function", + "lineNumber": 52, + "columnNumber": 3 + }, + { + "id": "def-public.ExampleClass.Unnamed.$1", + "label": "t", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Uncategorized", + "lineNumber": 52, + "columnNumber": 15 + }, + { + "id": "def-public.CrazyClass", + "label": "CrazyClass", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Class", + "lineNumber": 71, + "columnNumber": 1 + }, + { + "id": "def-public.ExampleInterface.anOptionalFn.$1", + "label": "foo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "string", + "lineNumber": 94, + "columnNumber": 19 + }, + { + "id": "def-public.ExampleInterface.fnTypeWithGeneric.$1", + "label": "t", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Uncategorized", + "lineNumber": 36, + "columnNumber": 37 + }, + { + "id": "def-public.ExampleInterface.fnTypeWithGeneric.$2", + "label": "p", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Object", + "lineNumber": 36, + "columnNumber": 43 + }, + { + "id": "def-public.IReturnAReactComponent.component", + "label": "component", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "CompoundType", + "lineNumber": 117, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.notAnArrowFn.$1", + "label": "a", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 24, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.notAnArrowFn.$2", + "label": "b", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "number", + "lineNumber": 25, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.notAnArrowFn.$3", + "label": "c", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Array", + "lineNumber": 26, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.notAnArrowFn.$4", + "label": "d", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "CompoundType", + "lineNumber": 27, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.notAnArrowFn.$5", + "label": "e", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 28, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyMisdirection.$1", + "label": "a", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 24, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyMisdirection.$2", + "label": "b", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "number", + "lineNumber": 25, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyMisdirection.$3", + "label": "c", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Array", + "lineNumber": 26, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyMisdirection.$4", + "label": "d", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "CompoundType", + "lineNumber": 27, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyMisdirection.$5", + "label": "e", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 28, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyInlineFn.$1", + "label": "a", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "CompoundType", + "lineNumber": 32, + "columnNumber": 23 + }, + { + "id": "def-public.aPretendNamespaceObj.nestedObj.foo", + "label": "foo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "string", + "lineNumber": 45, + "columnNumber": 5 + }, + { + "id": "def-public.TypeWithGeneric", + "label": "TypeWithGeneric", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Type", + "lineNumber": 19, + "columnNumber": 1 + }, + { + "id": "def-public.ImAType", + "label": "ImAType", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Type", + "lineNumber": 21, + "columnNumber": 1 + }, + { + "id": "def-public.FnTypeWithGeneric.$2", + "label": "p", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Object", + "lineNumber": 36, + "columnNumber": 43 + }, + { + "id": "def-public.IRefANotExportedType", + "label": "IRefANotExportedType", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Type", + "lineNumber": 52, + "columnNumber": 1 + }, + { + "id": "def-public.ImAnObject", + "label": "ImAnObject", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Interface", + "lineNumber": 53, + "columnNumber": 1 + }, + { + "id": "def-public.ImAnObject.foo", + "label": "foo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Function", + "lineNumber": 54, + "columnNumber": 3 + }, + { + "id": "def-public.ImAnObject.foo.$1", + "label": "t", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Uncategorized", + "lineNumber": 28, + "columnNumber": 33 + }, + { + "id": "def-public.MyProps", + "label": "MyProps", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Interface", + "lineNumber": 60, + "columnNumber": 1 + }, + { + "id": "def-public.MyProps.foo", + "label": "foo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "string", + "lineNumber": 61, + "columnNumber": 3 + }, + { + "id": "def-public.MyProps.bar", + "label": "bar", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Function", + "lineNumber": 62, + "columnNumber": 3 + }, + { + "id": "def-public.MyProps.bar.$1", + "label": "t", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Uncategorized", + "lineNumber": 28, + "columnNumber": 33 + }, + { + "id": "def-public.AReactElementFn", + "label": "AReactElementFn", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Type", + "lineNumber": 65, + "columnNumber": 1 + }, + { + "id": "def-common.commonFoo", + "label": "commonFoo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/foo/index.ts", + "type": "string", + "lineNumber": 10, + "columnNumber": 14 + }, + { + "id": "def-common.ImACommonType", + "label": "ImACommonType", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/index.ts", + "type": "Interface", + "lineNumber": 12, + "columnNumber": 1 + }, + { + "id": "def-common.ImACommonType.goo", + "label": "goo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/index.ts", + "type": "number", + "lineNumber": 13, + "columnNumber": 3 + } + ], + "isAnyType": [ + { + "id": "def-public.imAnAny", + "label": "imAnAny", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "Any", + "lineNumber": 20, + "columnNumber": 14 + } + ], + "noReferences": [ + { + "id": "def-public.Setup.getSearchService.$1", + "label": "searchSpec", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Object", + "lineNumber": 102, + "columnNumber": 22 + }, + { + "id": "def-public.Setup.getSearchService", + "label": "getSearchService", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Function", + "lineNumber": 91, + "columnNumber": 3 + }, + { + "id": "def-public.Setup.getSearchService2.$1.username", + "label": "username", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "string", + "lineNumber": 110, + "columnNumber": 37 + }, + { + "id": "def-public.Setup.getSearchService2.$1.password", + "label": "password", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "string", + "lineNumber": 110, + "columnNumber": 55 + }, + { + "id": "def-public.Setup.getSearchService2.$1", + "label": "searchSpec", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Object", + "lineNumber": 110, + "columnNumber": 35 + }, + { + "id": "def-public.Setup.getSearchService2", + "label": "getSearchService2", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Function", + "lineNumber": 104, + "columnNumber": 3 + }, + { + "id": "def-public.Setup.doTheThing.$1", + "label": "thingOne", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "number", + "lineNumber": 123, + "columnNumber": 16 + }, + { + "id": "def-public.Setup.doTheThing.$2", + "label": "thingTwo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "string", + "lineNumber": 123, + "columnNumber": 34 + }, + { + "id": "def-public.Setup.doTheThing.$3.nestedVar", + "label": "nestedVar", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "number", + "lineNumber": 123, + "columnNumber": 66 + }, + { + "id": "def-public.Setup.doTheThing.$3", + "label": "thingThree", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Object", + "lineNumber": 123, + "columnNumber": 64 + }, + { + "id": "def-public.Setup.doTheThing", + "label": "doTheThing", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Function", + "lineNumber": 112, + "columnNumber": 3 + }, + { + "id": "def-public.Setup.fnWithInlineParams.$1.fn.$1.param", + "label": "param", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "string", + "lineNumber": 135, + "columnNumber": 17 + }, + { + "id": "def-public.Setup.fnWithInlineParams.$1.fn.$1", + "label": "foo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Object", + "lineNumber": 135, + "columnNumber": 15 + }, + { + "id": "def-public.Setup.fnWithInlineParams.$1.fn", + "label": "fn", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Function", + "lineNumber": 135, + "columnNumber": 5 + }, + { + "id": "def-public.Setup.fnWithInlineParams.$1", + "label": "obj", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Object", + "lineNumber": 134, + "columnNumber": 29 + }, + { + "id": "def-public.Setup.fnWithInlineParams", + "label": "fnWithInlineParams", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Function", + "lineNumber": 125, + "columnNumber": 3 + }, + { + "id": "def-public.Setup.id", + "label": "id", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "string", + "lineNumber": 138, + "columnNumber": 3 + }, + { + "id": "def-public.Setup", + "label": "Setup", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Interface", + "lineNumber": 77, + "columnNumber": 1 + }, + { + "id": "def-public.Start.getSearchLanguage", + "label": "getSearchLanguage", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Function", + "lineNumber": 66, + "columnNumber": 3 + }, + { + "id": "def-public.Start", + "label": "Start", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Interface", + "lineNumber": 52, + "columnNumber": 1 + }, + { + "id": "def-public.SearchSpec.username", + "label": "username", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "string", + "lineNumber": 24, + "columnNumber": 3 + }, + { + "id": "def-public.SearchSpec.password", + "label": "password", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "string", + "lineNumber": 28, + "columnNumber": 3 + }, + { + "id": "def-public.SearchSpec", + "label": "SearchSpec", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/plugin.ts", + "type": "Interface", + "lineNumber": 19, + "columnNumber": 1 + }, + { + "id": "def-public.doTheFooFnThing", + "label": "doTheFooFnThing", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/foo/index.ts", + "type": "Function", + "lineNumber": 10, + "columnNumber": 14 + }, + { + "id": "def-public.FooType", + "label": "FooType", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/foo/index.ts", + "type": "Type", + "lineNumber": 12, + "columnNumber": 1 + }, + { + "id": "def-public.imAnAny", + "label": "imAnAny", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "Any", + "lineNumber": 20, + "columnNumber": 14 + }, + { + "id": "def-public.imAnUnknown", + "label": "imAnUnknown", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "Unknown", + "lineNumber": 21, + "columnNumber": 14 + }, + { + "id": "def-public.InterfaceWithIndexSignature.Unnamed", + "label": "[key: string]: { foo: string; }", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "IndexSignature", + "lineNumber": 25, + "columnNumber": 3 + }, + { + "id": "def-public.InterfaceWithIndexSignature", + "label": "InterfaceWithIndexSignature", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "Interface", + "lineNumber": 24, + "columnNumber": 1 + }, + { + "id": "def-public.ClassConstructorWithStaticProperties.staticProperty1", + "label": "staticProperty1", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "string", + "lineNumber": 29, + "columnNumber": 3 + }, + { + "id": "def-public.ClassConstructorWithStaticProperties.new.$1.foo", + "label": "foo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "string", + "lineNumber": 30, + "columnNumber": 18 + }, + { + "id": "def-public.ClassConstructorWithStaticProperties.new.$1", + "label": "config", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "Object", + "lineNumber": 30, + "columnNumber": 16 + }, + { + "id": "def-public.ClassConstructorWithStaticProperties.new", + "label": "new", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "Function", + "lineNumber": 30, + "columnNumber": 3 + }, + { + "id": "def-public.ClassConstructorWithStaticProperties", + "label": "ClassConstructorWithStaticProperties", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/index.ts", + "type": "Interface", + "lineNumber": 28, + "columnNumber": 1 + }, + { + "id": "def-public.notAnArrowFn.$1", + "label": "a", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 24, + "columnNumber": 3 + }, + { + "id": "def-public.notAnArrowFn.$2", + "label": "b", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "number", + "lineNumber": 25, + "columnNumber": 3 + }, + { + "id": "def-public.notAnArrowFn.$3", + "label": "c", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Array", + "lineNumber": 26, + "columnNumber": 3 + }, + { + "id": "def-public.notAnArrowFn.$4", + "label": "d", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "CompoundType", + "lineNumber": 27, + "columnNumber": 3 + }, + { + "id": "def-public.notAnArrowFn.$5", + "label": "e", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 28, + "columnNumber": 3 + }, + { + "id": "def-public.notAnArrowFn", + "label": "notAnArrowFn", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Function", + "lineNumber": 13, + "columnNumber": 1 + }, + { + "id": "def-public.arrowFn.$1", + "label": "a", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 44, + "columnNumber": 3 + }, + { + "id": "def-public.arrowFn.$2", + "label": "b", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "number", + "lineNumber": 45, + "columnNumber": 3 + }, + { + "id": "def-public.arrowFn.$3", + "label": "c", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Array", + "lineNumber": 46, + "columnNumber": 3 + }, + { + "id": "def-public.arrowFn.$4", + "label": "d", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "CompoundType", + "lineNumber": 47, + "columnNumber": 3 + }, + { + "id": "def-public.arrowFn.$5", + "label": "e", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 48, + "columnNumber": 3 + }, + { + "id": "def-public.arrowFn", + "label": "arrowFn", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Function", + "lineNumber": 43, + "columnNumber": 14 + }, + { + "id": "def-public.crazyFunction.$1.hi", + "label": "hi", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 66, + "columnNumber": 12 + }, + { + "id": "def-public.crazyFunction.$1", + "label": "obj", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Object", + "lineNumber": 66, + "columnNumber": 10 + }, + { + "id": "def-public.crazyFunction.$2.fn1.$1.param", + "label": "param", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 67, + "columnNumber": 34 + }, + { + "id": "def-public.crazyFunction.$2.fn1.$1", + "label": "foo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Object", + "lineNumber": 67, + "columnNumber": 32 + }, + { + "id": "def-public.crazyFunction.$2.fn1", + "label": "fn1", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Function", + "lineNumber": 67, + "columnNumber": 21 + }, + { + "id": "def-public.crazyFunction.$2.fn2", + "label": "fn2", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Function", + "lineNumber": 67, + "columnNumber": 62 + }, + { + "id": "def-public.crazyFunction.$2", + "label": "{ fn1, fn2 }", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Object", + "lineNumber": 67, + "columnNumber": 19 + }, + { + "id": "def-public.crazyFunction.$3.str", + "label": "str", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 68, + "columnNumber": 16 + }, + { + "id": "def-public.crazyFunction.$3", + "label": "{ str }", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Object", + "lineNumber": 68, + "columnNumber": 14 + }, + { + "id": "def-public.crazyFunction", + "label": "crazyFunction", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Function", + "lineNumber": 64, + "columnNumber": 14 + }, + { + "id": "def-public.fnWithNonExportedRef.$1", + "label": "a", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Function", + "lineNumber": 74, + "columnNumber": 38 + }, + { + "id": "def-public.fnWithNonExportedRef", + "label": "fnWithNonExportedRef", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Function", + "lineNumber": 74, + "columnNumber": 14 + }, + { + "id": "def-public.NotAnArrowFnType.$1", + "label": "a", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 24, + "columnNumber": 3 + }, + { + "id": "def-public.NotAnArrowFnType.$2", + "label": "b", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "number", + "lineNumber": 25, + "columnNumber": 3 + }, + { + "id": "def-public.NotAnArrowFnType.$3", + "label": "c", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Array", + "lineNumber": 26, + "columnNumber": 3 + }, + { + "id": "def-public.NotAnArrowFnType.$4", + "label": "d", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "CompoundType", + "lineNumber": 27, + "columnNumber": 3 + }, + { + "id": "def-public.NotAnArrowFnType.$5", + "label": "e", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 28, + "columnNumber": 3 + }, + { + "id": "def-public.NotAnArrowFnType", + "label": "NotAnArrowFnType", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Type", + "lineNumber": 76, + "columnNumber": 1 + }, + { + "id": "def-public.WithGen.t", + "label": "t", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Uncategorized", + "lineNumber": 32, + "columnNumber": 3 + }, + { + "id": "def-public.WithGen", + "label": "WithGen", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Interface", + "lineNumber": 28, + "columnNumber": 1 + }, + { + "id": "def-public.AnotherInterface.t", + "label": "t", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Uncategorized", + "lineNumber": 41, + "columnNumber": 3 + }, + { + "id": "def-public.ExampleClass.component", + "label": "component", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "CompoundType", + "lineNumber": 50, + "columnNumber": 3 + }, + { + "id": "def-public.ExampleClass.Unnamed.$1", + "label": "t", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Uncategorized", + "lineNumber": 52, + "columnNumber": 15 + }, + { + "id": "def-public.ExampleClass.Unnamed", + "label": "Constructor", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Function", + "lineNumber": 52, + "columnNumber": 3 + }, + { + "id": "def-public.ExampleClass.arrowFn.$1", + "label": "a", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "CompoundType", + "lineNumber": 60, + "columnNumber": 14 + }, + { + "id": "def-public.ExampleClass.arrowFn", + "label": "arrowFn", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Function", + "lineNumber": 56, + "columnNumber": 3 + }, + { + "id": "def-public.ExampleClass.getVar.$1", + "label": "a", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "CompoundType", + "lineNumber": 66, + "columnNumber": 10 + }, + { + "id": "def-public.ExampleClass.getVar", + "label": "getVar", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Function", + "lineNumber": 62, + "columnNumber": 3 + }, + { + "id": "def-public.ExampleClass", + "label": "ExampleClass", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Class", + "lineNumber": 44, + "columnNumber": 1 + }, + { + "id": "def-public.CrazyClass", + "label": "CrazyClass", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Class", + "lineNumber": 71, + "columnNumber": 1 + }, + { + "id": "def-public.ExampleInterface.getAPromiseThatResolvesToString", + "label": "getAPromiseThatResolvesToString", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Function", + "lineNumber": 78, + "columnNumber": 3 + }, + { + "id": "def-public.ExampleInterface.aFnWithGen.$1", + "label": "t", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Uncategorized", + "lineNumber": 89, + "columnNumber": 19 + }, + { + "id": "def-public.ExampleInterface.aFnWithGen", + "label": "aFnWithGen", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Function", + "lineNumber": 83, + "columnNumber": 3 + }, + { + "id": "def-public.ExampleInterface.anOptionalFn.$1", + "label": "foo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "string", + "lineNumber": 94, + "columnNumber": 19 + }, + { + "id": "def-public.ExampleInterface.anOptionalFn", + "label": "anOptionalFn", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Function", + "lineNumber": 91, + "columnNumber": 3 + }, + { + "id": "def-public.ExampleInterface.aFn", + "label": "aFn", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Function", + "lineNumber": 96, + "columnNumber": 3 + }, + { + "id": "def-public.ExampleInterface.fnTypeWithGeneric.$1", + "label": "t", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Uncategorized", + "lineNumber": 36, + "columnNumber": 37 + }, + { + "id": "def-public.ExampleInterface.fnTypeWithGeneric.$2", + "label": "p", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Object", + "lineNumber": 36, + "columnNumber": 43 + }, + { + "id": "def-public.ExampleInterface.fnTypeWithGeneric", + "label": "fnTypeWithGeneric", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Function", + "lineNumber": 101, + "columnNumber": 3 + }, + { + "id": "def-public.ExampleInterface.fnTypeWithGenericThatIsOptional", + "label": "fnTypeWithGenericThatIsOptional", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Function", + "lineNumber": 107, + "columnNumber": 3 + }, + { + "id": "def-public.ExampleInterface", + "label": "ExampleInterface", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Interface", + "lineNumber": 73, + "columnNumber": 1 + }, + { + "id": "def-public.IReturnAReactComponent.component", + "label": "component", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "CompoundType", + "lineNumber": 117, + "columnNumber": 3 + }, + { + "id": "def-public.IReturnAReactComponent", + "label": "IReturnAReactComponent", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts", + "type": "Interface", + "lineNumber": 113, + "columnNumber": 1 + }, + { + "id": "def-public.aPretendNamespaceObj.notAnArrowFn.$1", + "label": "a", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 24, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.notAnArrowFn.$2", + "label": "b", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "number", + "lineNumber": 25, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.notAnArrowFn.$3", + "label": "c", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Array", + "lineNumber": 26, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.notAnArrowFn.$4", + "label": "d", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "CompoundType", + "lineNumber": 27, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.notAnArrowFn.$5", + "label": "e", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 28, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.notAnArrowFn", + "label": "notAnArrowFn", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "Function", + "lineNumber": 19, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyMisdirection.$1", + "label": "a", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 24, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyMisdirection.$2", + "label": "b", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "number", + "lineNumber": 25, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyMisdirection.$3", + "label": "c", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Array", + "lineNumber": 26, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyMisdirection.$4", + "label": "d", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "CompoundType", + "lineNumber": 27, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyMisdirection.$5", + "label": "e", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "string", + "lineNumber": 28, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyMisdirection", + "label": "aPropertyMisdirection", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "Function", + "lineNumber": 24, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyInlineFn.$1", + "label": "a", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "CompoundType", + "lineNumber": 32, + "columnNumber": 23 + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyInlineFn", + "label": "aPropertyInlineFn", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "Function", + "lineNumber": 29, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyStr", + "label": "aPropertyStr", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "string", + "lineNumber": 36, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj.nestedObj.foo", + "label": "foo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "string", + "lineNumber": 45, + "columnNumber": 5 + }, + { + "id": "def-public.aPretendNamespaceObj.nestedObj", + "label": "nestedObj", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "Object", + "lineNumber": 41, + "columnNumber": 3 + }, + { + "id": "def-public.aPretendNamespaceObj", + "label": "aPretendNamespaceObj", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "Object", + "lineNumber": 18, + "columnNumber": 14 + }, + { + "id": "def-public.aUnionProperty", + "label": "aUnionProperty", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "CompoundType", + "lineNumber": 59, + "columnNumber": 14 + }, + { + "id": "def-public.aStrArray", + "label": "aStrArray", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "Array", + "lineNumber": 64, + "columnNumber": 14 + }, + { + "id": "def-public.aNumArray", + "label": "aNumArray", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "Array", + "lineNumber": 69, + "columnNumber": 14 + }, + { + "id": "def-public.aStr", + "label": "aStr", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "string", + "lineNumber": 74, + "columnNumber": 14 + }, + { + "id": "def-public.aNum", + "label": "aNum", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "number", + "lineNumber": 79, + "columnNumber": 14 + }, + { + "id": "def-public.literalString", + "label": "literalString", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "type": "string", + "lineNumber": 84, + "columnNumber": 14 + }, + { + "id": "def-public.StringOrUndefinedType", + "label": "StringOrUndefinedType", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Type", + "lineNumber": 14, + "columnNumber": 1 + }, + { + "id": "def-public.TypeWithGeneric", + "label": "TypeWithGeneric", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Type", + "lineNumber": 19, + "columnNumber": 1 + }, + { + "id": "def-public.ImAType", + "label": "ImAType", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Type", + "lineNumber": 21, + "columnNumber": 1 + }, + { + "id": "def-public.FnWithGeneric.$1", + "label": "t", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Uncategorized", + "lineNumber": 28, + "columnNumber": 33 + }, + { + "id": "def-public.FnWithGeneric", + "label": "FnWithGeneric", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Type", + "lineNumber": 23, + "columnNumber": 1 + }, + { + "id": "def-public.FnTypeWithGeneric.$1", + "label": "t", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Uncategorized", + "lineNumber": 36, + "columnNumber": 37 + }, + { + "id": "def-public.FnTypeWithGeneric.$2", + "label": "p", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Object", + "lineNumber": 36, + "columnNumber": 43 + }, + { + "id": "def-public.FnTypeWithGeneric", + "label": "FnTypeWithGeneric", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Type", + "lineNumber": 30, + "columnNumber": 1 + }, + { + "id": "def-public.DayOfWeek", + "label": "DayOfWeek", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Enum", + "lineNumber": 38, + "columnNumber": 1 + }, + { + "id": "def-public.MultipleDeclarationsType", + "label": "MultipleDeclarationsType", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Type", + "lineNumber": 47, + "columnNumber": 1 + }, + { + "id": "def-public.IRefANotExportedType", + "label": "IRefANotExportedType", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Type", + "lineNumber": 52, + "columnNumber": 1 + }, + { + "id": "def-public.ImAnObject.foo.$1", + "label": "t", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Uncategorized", + "lineNumber": 28, + "columnNumber": 33 + }, + { + "id": "def-public.ImAnObject.foo", + "label": "foo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Function", + "lineNumber": 54, + "columnNumber": 3 + }, + { + "id": "def-public.ImAnObject", + "label": "ImAnObject", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Interface", + "lineNumber": 53, + "columnNumber": 1 + }, + { + "id": "def-public.MyProps.foo", + "label": "foo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "string", + "lineNumber": 61, + "columnNumber": 3 + }, + { + "id": "def-public.MyProps.bar.$1", + "label": "t", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Uncategorized", + "lineNumber": 28, + "columnNumber": 33 + }, + { + "id": "def-public.MyProps.bar", + "label": "bar", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Function", + "lineNumber": 62, + "columnNumber": 3 + }, + { + "id": "def-public.MyProps", + "label": "MyProps", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Interface", + "lineNumber": 60, + "columnNumber": 1 + }, + { + "id": "def-public.AReactElementFn", + "label": "AReactElementFn", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts", + "type": "Type", + "lineNumber": 65, + "columnNumber": 1 + }, + { + "id": "def-common.commonFoo", + "label": "commonFoo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/foo/index.ts", + "type": "string", + "lineNumber": 10, + "columnNumber": 14 + }, + { + "id": "def-common.ImACommonType.goo", + "label": "goo", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/index.ts", + "type": "number", + "lineNumber": 13, + "columnNumber": 3 + }, + { + "id": "def-common.ImACommonType", + "label": "ImACommonType", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/index.ts", + "type": "Interface", + "lineNumber": 12, + "columnNumber": 1 + } + ] +} diff --git a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a_foo.devdocs.json b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a_foo.devdocs.json index 331cf1783e8c4..4a56ef46b7a73 100644 --- a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a_foo.devdocs.json +++ b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a_foo.devdocs.json @@ -14,6 +14,8 @@ "() => void" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/foo/index.ts", + "lineNumber": 10, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "children": [], @@ -35,6 +37,8 @@ "() => \"foo\"" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/foo/index.ts", + "lineNumber": 12, + "columnNumber": 1, "deprecated": false, "trackAdoption": false, "returnComment": [], @@ -69,6 +73,8 @@ "\"COMMON VAR!\"" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/common/foo/index.ts", + "lineNumber": 10, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -76,4 +82,4 @@ ], "objects": [] } -} +} \ No newline at end of file diff --git a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a_foo.mdx b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a_foo.mdx index 719f998adc203..0f89d454cf2ab 100644 --- a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a_foo.mdx +++ b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a_foo.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/pluginA-foo title: "pluginA.foo" image: https://source.unsplash.com/400x175/?github description: API docs for the pluginA.foo plugin -date: 2022-09-05 +date: 2025-12-31 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginA.foo'] --- import pluginAFooObj from './plugin_a_foo.devdocs.json'; diff --git a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_b.devdocs.json b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_b.devdocs.json index 5e27a43af3866..f3b532646d926 100644 --- a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_b.devdocs.json +++ b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_b.devdocs.json @@ -30,6 +30,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_b/public/index.ts", + "lineNumber": 12, + "columnNumber": 14, "deprecated": false, "trackAdoption": false, "children": [ @@ -51,6 +53,8 @@ "" ], "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_b/public/index.ts", + "lineNumber": 12, + "columnNumber": 25, "deprecated": false, "trackAdoption": false, "isRequired": true @@ -81,4 +85,4 @@ "misc": [], "objects": [] } -} +} \ No newline at end of file diff --git a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_b.mdx b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_b.mdx index 9a06228dfd688..b66ffe4301d1a 100644 --- a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_b.mdx +++ b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_b.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/pluginB title: "pluginB" image: https://source.unsplash.com/400x175/?github description: API docs for the pluginB plugin -date: 2022-09-05 +date: 2025-12-31 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginB'] --- import pluginBObj from './plugin_b.devdocs.json'; diff --git a/packages/kbn-docs-utils/src/mdx/build_plugin_deprecations_table.test.ts b/packages/kbn-docs-utils/src/mdx/build_plugin_deprecations_table.test.ts new file mode 100644 index 0000000000000..b837ee0c876b3 --- /dev/null +++ b/packages/kbn-docs-utils/src/mdx/build_plugin_deprecations_table.test.ts @@ -0,0 +1,266 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ToolingLog } from '@kbn/tooling-log'; +import { buildPluginDeprecationsTable } from './build_plugin_deprecations_table'; +import type { ReferencedDeprecationsByPlugin, ApiDeclaration, ApiReference } from '../types'; +import { createMockApiDeclaration, createMockReference } from '../__test_helpers__/mocks'; + +const log = new ToolingLog({ + level: 'debug', + writeTo: { write: () => {} }, +}); + +describe('buildPluginDeprecationsTable', () => { + it('returns empty string for empty deprecations', () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = {}; + + const result = buildPluginDeprecationsTable(deprecationsByPlugin, log); + + expect(result).toBe(''); + }); + + it('builds table with single plugin deprecation', () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ id: 'api1', label: 'deprecatedFn' }), + ref: createMockReference(), + }, + ], + }; + + const result = buildPluginDeprecationsTable(deprecationsByPlugin, log); + + expect(result).toContain('## pluginA'); + expect(result).toContain('| Deprecated API | Reference location(s) | Remove By |'); + expect(result).toContain('deprecatedFn'); + }); + + it('groups multiple references to the same deprecated API', () => { + const deprecatedApi = createMockApiDeclaration({ id: 'api1', label: 'sharedDeprecatedFn' }); + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { deprecatedApi, ref: createMockReference({ path: 'src/plugins/a/file1.ts' }) }, + { deprecatedApi, ref: createMockReference({ path: 'src/plugins/a/file2.ts' }) }, + ], + }; + + const result = buildPluginDeprecationsTable(deprecationsByPlugin, log); + + expect(result).toContain('sharedDeprecatedFn'); + expect(result).toContain('file1.ts'); + expect(result).toContain('file2.ts'); + }); + + it('includes removeBy date when present', () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'deprecatedWithDate', + removeBy: '9.0.0', + }), + ref: createMockReference(), + }, + ], + }; + + const result = buildPluginDeprecationsTable(deprecationsByPlugin, log); + + expect(result).toContain('9.0.0'); + }); + + it('shows dash when removeBy is not present', () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'deprecatedNoDate', + removeBy: undefined, + }), + ref: createMockReference(), + }, + ], + }; + + const result = buildPluginDeprecationsTable(deprecationsByPlugin, log); + + expect(result).toContain('| - |'); + }); + + it('sorts plugins alphabetically', () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + zebra: [ + { + deprecatedApi: createMockApiDeclaration({ id: 'api1', label: 'zebraApi' }), + ref: createMockReference(), + }, + ], + alpha: [ + { + deprecatedApi: createMockApiDeclaration({ id: 'api2', label: 'alphaApi' }), + ref: createMockReference(), + }, + ], + }; + + const result = buildPluginDeprecationsTable(deprecationsByPlugin, log); + + const alphaIndex = result.indexOf('## alpha'); + const zebraIndex = result.indexOf('## zebra'); + expect(alphaIndex).toBeLessThan(zebraIndex); + }); + + it('includes DocLink with correct plugin API doc ID', () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'myDeprecatedFn', + parentPluginId: 'testPlugin', + }), + ref: createMockReference(), + }, + ], + }; + + const result = buildPluginDeprecationsTable(deprecationsByPlugin, log); + + expect(result).toContain(' { + const deprecatedApi = createMockApiDeclaration({ id: 'api1', label: 'popularDeprecatedFn' }); + const refs: Array<{ deprecatedApi: ApiDeclaration; ref: ApiReference }> = []; + for (let i = 0; i < 15; i++) { + refs.push({ + deprecatedApi, + ref: createMockReference({ path: `src/plugins/a/file${i}.ts` }), + }); + } + + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: refs, + }; + + const result = buildPluginDeprecationsTable(deprecationsByPlugin, log); + + expect(result).toContain('+ 5 more'); + }); + + it('creates GitHub links for reference locations', () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ id: 'api1', label: 'linkedFn' }), + ref: createMockReference({ path: 'src/plugins/a/specific_file.ts' }), + }, + ], + }; + + const result = buildPluginDeprecationsTable(deprecationsByPlugin, log); + + expect(result).toContain('https://github.com/elastic/kibana/tree/main/'); + expect(result).toContain('specific_file.ts'); + }); + + it('handles multiple plugins with multiple deprecations', () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ id: 'api1', label: 'deprecatedA1' }), + ref: createMockReference(), + }, + { + deprecatedApi: createMockApiDeclaration({ id: 'api2', label: 'deprecatedA2' }), + ref: createMockReference(), + }, + ], + pluginB: [ + { + deprecatedApi: createMockApiDeclaration({ id: 'api3', label: 'deprecatedB1' }), + ref: createMockReference(), + }, + ], + }; + + const result = buildPluginDeprecationsTable(deprecationsByPlugin, log); + + expect(result).toContain('## pluginA'); + expect(result).toContain('## pluginB'); + expect(result).toContain('deprecatedA1'); + expect(result).toContain('deprecatedA2'); + expect(result).toContain('deprecatedB1'); + }); + + it('does not mutate the input deprecationsByPlugin object', () => { + const deprecatedApi = createMockApiDeclaration({ id: 'api1', label: 'mutationTest' }); + const originalRefs = Array.from({ length: 15 }, (_, i) => ({ + deprecatedApi, + ref: createMockReference({ path: `src/plugins/a/file${i}.ts` }), + })); + + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [...originalRefs], + }; + + const beforeLength = deprecationsByPlugin.pluginA.length; + buildPluginDeprecationsTable(deprecationsByPlugin, log); + + expect(deprecationsByPlugin.pluginA).toHaveLength(beforeLength); + }); + + it('shows all references without truncation message when exactly 10 exist', () => { + const deprecatedApi = createMockApiDeclaration({ id: 'api1', label: 'exactlyTenFn' }); + const refs: Array<{ deprecatedApi: ApiDeclaration; ref: ApiReference }> = Array.from( + { length: 10 }, + (_, i) => ({ + deprecatedApi, + ref: createMockReference({ path: `src/plugins/a/file${i}.ts` }), + }) + ); + + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: refs, + }; + + const result = buildPluginDeprecationsTable(deprecationsByPlugin, log); + + expect(result).not.toContain('+ 0 more'); + expect(result).not.toContain('more'); + expect(result).toContain('file9.ts'); + }); + + it('shows all references without truncation message when fewer than 10 exist', () => { + const deprecatedApi = createMockApiDeclaration({ id: 'api1', label: 'fewRefsFn' }); + const refs: Array<{ deprecatedApi: ApiDeclaration; ref: ApiReference }> = Array.from( + { length: 5 }, + (_, i) => ({ + deprecatedApi, + ref: createMockReference({ path: `src/plugins/a/file${i}.ts` }), + }) + ); + + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: refs, + }; + + const result = buildPluginDeprecationsTable(deprecationsByPlugin, log); + + expect(result).not.toContain('more'); + expect(result).toContain('file0.ts'); + expect(result).toContain('file4.ts'); + }); +}); diff --git a/packages/kbn-docs-utils/src/mdx/build_plugin_deprecations_table.ts b/packages/kbn-docs-utils/src/mdx/build_plugin_deprecations_table.ts index a3ad181b08ffe..562978d684b31 100644 --- a/packages/kbn-docs-utils/src/mdx/build_plugin_deprecations_table.ts +++ b/packages/kbn-docs-utils/src/mdx/build_plugin_deprecations_table.ts @@ -8,14 +8,13 @@ */ import type { ToolingLog } from '@kbn/tooling-log'; -import Path from 'path'; +import path from 'path'; import type { ApiDeclaration, ApiReference, ReferencedDeprecationsByPlugin } from '../types'; import { getPluginApiDocId } from '../utils'; export function buildPluginDeprecationsTable( - folder: string, deprecationsByPlugin: ReferencedDeprecationsByPlugin, - log: ToolingLog + _log: ToolingLog ): string { const tableMdx = Object.keys(deprecationsByPlugin) .sort() @@ -44,18 +43,19 @@ export function buildPluginDeprecationsTable( api.parentPluginId )}" section="${api.id}" text="${api.label}"/>`; - const firstTen = refs.splice(0, 10); + const firstTen = refs.slice(0, 10); + const remainingCount = refs.length - 10; const referencedLocations = firstTen .map( (ref) => `[${ref.path.substr( - ref.path.lastIndexOf(Path.sep) + 1 + ref.path.lastIndexOf(path.sep) + 1 )}](https://github.com/elastic/kibana/tree/main/${ ref.path }#:~:text=${encodeURIComponent(api.label)})` ) - .join(', ') + (refs.length > 0 ? `+ ${refs.length} more` : ''); + .join(', ') + (remainingCount > 0 ? `+ ${remainingCount} more` : ''); const removeBy = api.removeBy ? api.removeBy : '-'; diff --git a/packages/kbn-docs-utils/src/mdx/get_all_doc_file_ids.test.ts b/packages/kbn-docs-utils/src/mdx/get_all_doc_file_ids.test.ts new file mode 100644 index 0000000000000..96bbc3eb02b53 --- /dev/null +++ b/packages/kbn-docs-utils/src/mdx/get_all_doc_file_ids.test.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import Fsp from 'fs/promises'; +import globby from 'globby'; +import { getAllDocFileIds } from './get_all_doc_file_ids'; + +jest.mock('fs/promises'); +jest.mock('globby'); + +const mockFsp = Fsp as jest.Mocked; +const mockGlobby = globby as jest.MockedFunction; + +describe('getAllDocFileIds', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns doc IDs from mdx files with valid frontmatter', async () => { + mockGlobby.mockResolvedValue(['/output/plugin_a.mdx', '/output/plugin_b.mdx']); + + mockFsp.readFile.mockImplementation(async (filePath) => { + if (filePath === '/output/plugin_a.mdx') { + return `--- +id: kibPluginAPluginApi +slug: /kibana-dev-docs/api/pluginA +title: "pluginA" +--- +Content here`; + } + if (filePath === '/output/plugin_b.mdx') { + return `--- +id: kibPluginBPluginApi +slug: /kibana-dev-docs/api/pluginB +title: "pluginB" +--- +Content here`; + } + throw new Error(`Unexpected file: ${filePath}`); + }); + + const ids = await getAllDocFileIds('/output'); + + expect(ids).toEqual(['kibPluginAPluginApi', 'kibPluginBPluginApi']); + }); + + it('handles empty directory', async () => { + mockGlobby.mockResolvedValue([]); + + const ids = await getAllDocFileIds('/output'); + + expect(ids).toEqual([]); + }); + + it('throws error when frontmatter start is missing', async () => { + mockGlobby.mockResolvedValue(['/output/bad.mdx']); + mockFsp.readFile.mockResolvedValue('No frontmatter here'); + + await expect(getAllDocFileIds('/output')).rejects.toThrow( + 'unable to find start of frontmatter' + ); + }); + + it('throws error when frontmatter end is missing', async () => { + mockGlobby.mockResolvedValue(['/output/bad.mdx']); + mockFsp.readFile.mockResolvedValue(`--- +id: someId +No closing frontmatter`); + + await expect(getAllDocFileIds('/output')).rejects.toThrow('unable to find end of frontmatter'); + }); + + it('throws error when frontmatter contains invalid YAML', async () => { + mockGlobby.mockResolvedValue(['/output/bad.mdx']); + mockFsp.readFile.mockResolvedValue(`--- +id: [invalid yaml +--- +Content`); + + await expect(getAllDocFileIds('/output')).rejects.toThrow('unable to parse frontmatter'); + }); + + it('throws error when frontmatter is not an object', async () => { + mockGlobby.mockResolvedValue(['/output/bad.mdx']); + mockFsp.readFile.mockResolvedValue(`--- +just a string +--- +Content`); + + await expect(getAllDocFileIds('/output')).rejects.toThrow('expected yaml to produce an object'); + }); + + it('throws error when id is missing from frontmatter', async () => { + mockGlobby.mockResolvedValue(['/output/bad.mdx']); + mockFsp.readFile.mockResolvedValue(`--- +slug: /some/path +title: "Some Title" +--- +Content`); + + await expect(getAllDocFileIds('/output')).rejects.toThrow('missing "id" in frontmatter'); + }); + + it('throws error when id is not a string', async () => { + mockGlobby.mockResolvedValue(['/output/bad.mdx']); + mockFsp.readFile.mockResolvedValue(`--- +id: 123 +slug: /some/path +--- +Content`); + + await expect(getAllDocFileIds('/output')).rejects.toThrow('missing "id" in frontmatter'); + }); + + it('processes multiple files concurrently', async () => { + const files = Array.from({ length: 25 }, (_, i) => `/output/plugin_${i}.mdx`); + mockGlobby.mockResolvedValue(files); + + mockFsp.readFile.mockImplementation(async (filePath) => { + const match = (filePath as string).match(/plugin_(\d+)/); + const num = match ? match[1] : '0'; + return `--- +id: kibPlugin${num}PluginApi +slug: /path +--- +Content`; + }); + + const ids = await getAllDocFileIds('/output'); + + expect(ids).toHaveLength(25); + expect(ids).toContain('kibPlugin0PluginApi'); + expect(ids).toContain('kibPlugin24PluginApi'); + }); + + it('handles frontmatter with extra fields', async () => { + mockGlobby.mockResolvedValue(['/output/plugin.mdx']); + mockFsp.readFile.mockResolvedValue(`--- +id: kibTestPluginApi +slug: /kibana-dev-docs/api/test +title: "Test Plugin" +description: A test plugin description +date: 2025-01-01 +tags: ['contributor', 'dev', 'apidocs'] +image: https://example.com/image.png +--- +Content here`); + + const ids = await getAllDocFileIds('/output'); + + expect(ids).toEqual(['kibTestPluginApi']); + }); +}); diff --git a/packages/kbn-docs-utils/src/mdx/write_deprecations_doc_by_api.test.ts b/packages/kbn-docs-utils/src/mdx/write_deprecations_doc_by_api.test.ts new file mode 100644 index 0000000000000..d9fe9d28deb6b --- /dev/null +++ b/packages/kbn-docs-utils/src/mdx/write_deprecations_doc_by_api.test.ts @@ -0,0 +1,232 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import Fsp from 'fs/promises'; +import { ToolingLog } from '@kbn/tooling-log'; +import { writeDeprecationDocByApi } from './write_deprecations_doc_by_api'; +import type { ReferencedDeprecationsByPlugin, UnreferencedDeprecationsByPlugin } from '../types'; +import { createMockApiDeclaration, createMockReference } from '../__test_helpers__/mocks'; + +jest.mock('fs/promises'); + +const mockFsp = Fsp as jest.Mocked; + +const log = new ToolingLog({ + level: 'debug', + writeTo: { write: () => {} }, +}); + +describe('writeDeprecationDocByApi', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockFsp.writeFile.mockResolvedValue(undefined); + }); + + it('writes deprecations_by_api.mdx file', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ id: 'api1', label: 'deprecatedFn' }), + ref: createMockReference(), + }, + ], + }; + const unReferencedDeprecations: UnreferencedDeprecationsByPlugin = {}; + + await writeDeprecationDocByApi('/output', deprecationsByPlugin, unReferencedDeprecations, log); + + expect(mockFsp.writeFile).toHaveBeenCalledTimes(1); + const [filePath] = mockFsp.writeFile.mock.calls[0]; + expect(filePath).toContain('deprecations_by_api.mdx'); + }); + + it('includes referenced deprecated APIs section', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'deprecatedFn', + references: [createMockReference()], + }), + ref: createMockReference(), + }, + ], + }; + const unReferencedDeprecations: UnreferencedDeprecationsByPlugin = {}; + + await writeDeprecationDocByApi('/output', deprecationsByPlugin, unReferencedDeprecations, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('## Referenced deprecated APIs'); + expect(content).toContain('| Deprecated API | Referencing plugin(s) | Remove By |'); + }); + + it('includes unreferenced deprecated APIs section', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = {}; + const unReferencedDeprecations: UnreferencedDeprecationsByPlugin = { + testPlugin: [createMockApiDeclaration({ id: 'unusedApi', label: 'unusedDeprecatedFn' })], + }; + + await writeDeprecationDocByApi('/output', deprecationsByPlugin, unReferencedDeprecations, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('## Unreferenced deprecated APIs'); + expect(content).toContain('Safe to remove'); + expect(content).toContain('unusedDeprecatedFn'); + }); + + it('lists referencing plugins for each deprecated API', async () => { + const deprecatedApi = createMockApiDeclaration({ + id: 'api1', + label: 'sharedDeprecatedFn', + references: [ + createMockReference({ plugin: 'pluginX' }), + createMockReference({ plugin: 'pluginY' }), + ], + }); + + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [{ deprecatedApi, ref: createMockReference({ plugin: 'pluginX' }) }], + pluginB: [{ deprecatedApi, ref: createMockReference({ plugin: 'pluginY' }) }], + }; + const unReferencedDeprecations: UnreferencedDeprecationsByPlugin = {}; + + await writeDeprecationDocByApi('/output', deprecationsByPlugin, unReferencedDeprecations, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('pluginX'); + expect(content).toContain('pluginY'); + }); + + it('sorts deprecated APIs by removeBy date', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'laterRemoval', + removeBy: '9.0.0', + }), + ref: createMockReference(), + }, + { + deprecatedApi: createMockApiDeclaration({ + id: 'api2', + label: 'earlierRemoval', + removeBy: '8.0.0', + }), + ref: createMockReference(), + }, + ], + }; + const unReferencedDeprecations: UnreferencedDeprecationsByPlugin = {}; + + await writeDeprecationDocByApi('/output', deprecationsByPlugin, unReferencedDeprecations, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + const earlierIndex = (content as string).indexOf('earlierRemoval'); + const laterIndex = (content as string).indexOf('laterRemoval'); + expect(earlierIndex).toBeLessThan(laterIndex); + }); + + it('shows dash for APIs without removeBy date', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'noDateApi', + removeBy: undefined, + }), + ref: createMockReference(), + }, + ], + }; + const unReferencedDeprecations: UnreferencedDeprecationsByPlugin = {}; + + await writeDeprecationDocByApi('/output', deprecationsByPlugin, unReferencedDeprecations, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('| - |'); + }); + + it('includes DocLinks for deprecated APIs', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'linkedApi', + parentPluginId: 'testPlugin', + }), + ref: createMockReference(), + }, + ], + }; + const unReferencedDeprecations: UnreferencedDeprecationsByPlugin = {}; + + await writeDeprecationDocByApi('/output', deprecationsByPlugin, unReferencedDeprecations, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain(' { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = {}; + const unReferencedDeprecations: UnreferencedDeprecationsByPlugin = {}; + + await writeDeprecationDocByApi('/output', deprecationsByPlugin, unReferencedDeprecations, log); + + expect(mockFsp.writeFile).toHaveBeenCalled(); + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('## Referenced deprecated APIs'); + expect(content).toContain('## Unreferenced deprecated APIs'); + }); + + it('includes frontmatter with correct metadata', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = {}; + const unReferencedDeprecations: UnreferencedDeprecationsByPlugin = {}; + + await writeDeprecationDocByApi('/output', deprecationsByPlugin, unReferencedDeprecations, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('id: kibDevDocsDeprecationsByApi'); + expect(content).toContain('slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-api'); + expect(content).toContain('title: Deprecated API usage by API'); + }); + + it('deduplicates referencing plugins', async () => { + const deprecatedApi = createMockApiDeclaration({ + id: 'api1', + label: 'sharedApi', + references: [ + createMockReference({ plugin: 'samePlugin' }), + createMockReference({ plugin: 'samePlugin' }), + ], + }); + + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { deprecatedApi, ref: createMockReference({ plugin: 'samePlugin' }) }, + { deprecatedApi, ref: createMockReference({ plugin: 'samePlugin' }) }, + ], + }; + const unReferencedDeprecations: UnreferencedDeprecationsByPlugin = {}; + + await writeDeprecationDocByApi('/output', deprecationsByPlugin, unReferencedDeprecations, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + const matches = (content as string).match(/samePlugin/g); + // Should only appear once in the table row (deduplicated). + expect(matches?.length).toBeLessThanOrEqual(2); + }); +}); diff --git a/packages/kbn-docs-utils/src/mdx/write_deprecations_doc_by_plugin.test.ts b/packages/kbn-docs-utils/src/mdx/write_deprecations_doc_by_plugin.test.ts new file mode 100644 index 0000000000000..7ee9358969065 --- /dev/null +++ b/packages/kbn-docs-utils/src/mdx/write_deprecations_doc_by_plugin.test.ts @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import Fsp from 'fs/promises'; +import { ToolingLog } from '@kbn/tooling-log'; +import { writeDeprecationDocByPlugin } from './write_deprecations_doc_by_plugin'; +import type { ReferencedDeprecationsByPlugin, ApiDeclaration, ApiReference } from '../types'; +import { createMockApiDeclaration, createMockReference } from '../__test_helpers__/mocks'; + +jest.mock('fs/promises'); + +const mockFsp = Fsp as jest.Mocked; + +const log = new ToolingLog({ + level: 'debug', + writeTo: { write: () => {} }, +}); + +describe('writeDeprecationDocByPlugin', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockFsp.writeFile.mockResolvedValue(undefined); + }); + + it('writes deprecations_by_plugin.mdx file', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ id: 'api1', label: 'deprecatedFn' }), + ref: createMockReference(), + }, + ], + }; + + await writeDeprecationDocByPlugin('/output', deprecationsByPlugin, log); + + expect(mockFsp.writeFile).toHaveBeenCalledTimes(1); + const [filePath] = mockFsp.writeFile.mock.calls[0]; + expect(filePath).toContain('deprecations_by_plugin.mdx'); + }); + + it('creates section for each plugin', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ id: 'api1', label: 'deprecatedA' }), + ref: createMockReference(), + }, + ], + pluginB: [ + { + deprecatedApi: createMockApiDeclaration({ id: 'api2', label: 'deprecatedB' }), + ref: createMockReference(), + }, + ], + }; + + await writeDeprecationDocByPlugin('/output', deprecationsByPlugin, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('## pluginA'); + expect(content).toContain('## pluginB'); + }); + + it('sorts plugins alphabetically', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + zebra: [ + { + deprecatedApi: createMockApiDeclaration({ id: 'api1', label: 'zebraApi' }), + ref: createMockReference(), + }, + ], + alpha: [ + { + deprecatedApi: createMockApiDeclaration({ id: 'api2', label: 'alphaApi' }), + ref: createMockReference(), + }, + ], + }; + + await writeDeprecationDocByPlugin('/output', deprecationsByPlugin, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + const alphaIndex = (content as string).indexOf('## alpha'); + const zebraIndex = (content as string).indexOf('## zebra'); + expect(alphaIndex).toBeLessThan(zebraIndex); + }); + + it('includes table with deprecated API, references, and removeBy', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'deprecatedFn', + removeBy: '9.0.0', + }), + ref: createMockReference({ path: 'src/plugins/a/file.ts' }), + }, + ], + }; + + await writeDeprecationDocByPlugin('/output', deprecationsByPlugin, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('| Deprecated API | Reference location(s) | Remove By |'); + expect(content).toContain('deprecatedFn'); + expect(content).toContain('9.0.0'); + }); + + it('groups references for the same deprecated API', async () => { + const deprecatedApi = createMockApiDeclaration({ id: 'api1', label: 'sharedDeprecatedFn' }); + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { deprecatedApi, ref: createMockReference({ path: 'src/plugins/a/file1.ts' }) }, + { deprecatedApi, ref: createMockReference({ path: 'src/plugins/a/file2.ts' }) }, + ], + }; + + await writeDeprecationDocByPlugin('/output', deprecationsByPlugin, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('file1.ts'); + expect(content).toContain('file2.ts'); + }); + + it('shows dash when removeBy is not present', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'noDateApi', + removeBy: undefined, + }), + ref: createMockReference(), + }, + ], + }; + + await writeDeprecationDocByPlugin('/output', deprecationsByPlugin, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('| - |'); + }); + + it('truncates references to first 10 and shows remaining count', async () => { + const deprecatedApi = createMockApiDeclaration({ id: 'api1', label: 'popularApi' }); + const refs: Array<{ deprecatedApi: ApiDeclaration; ref: ApiReference }> = []; + for (let i = 0; i < 15; i++) { + refs.push({ + deprecatedApi, + ref: createMockReference({ path: `src/plugins/a/file${i}.ts` }), + }); + } + + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: refs, + }; + + await writeDeprecationDocByPlugin('/output', deprecationsByPlugin, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('+ 5 more'); + }); + + it('creates GitHub links for reference locations', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ id: 'api1', label: 'linkedFn' }), + ref: createMockReference({ path: 'src/plugins/a/specific_file.ts' }), + }, + ], + }; + + await writeDeprecationDocByPlugin('/output', deprecationsByPlugin, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('https://github.com/elastic/kibana/tree/main/'); + expect(content).toContain('specific_file.ts'); + }); + + it('includes DocLinks for deprecated APIs', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'linkedApi', + parentPluginId: 'testPlugin', + }), + ref: createMockReference(), + }, + ], + }; + + await writeDeprecationDocByPlugin('/output', deprecationsByPlugin, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain(' { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = {}; + + await writeDeprecationDocByPlugin('/output', deprecationsByPlugin, log); + + expect(mockFsp.writeFile).toHaveBeenCalled(); + }); + + it('includes frontmatter with correct metadata', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = {}; + + await writeDeprecationDocByPlugin('/output', deprecationsByPlugin, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('id: kibDevDocsDeprecationsByPlugin'); + expect(content).toContain('slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-plugin'); + expect(content).toContain('title: Deprecated API usage by plugin'); + }); +}); diff --git a/packages/kbn-docs-utils/src/mdx/write_deprecations_doc_by_plugin.ts b/packages/kbn-docs-utils/src/mdx/write_deprecations_doc_by_plugin.ts index abac1dfb4269e..9448abcadf129 100644 --- a/packages/kbn-docs-utils/src/mdx/write_deprecations_doc_by_plugin.ts +++ b/packages/kbn-docs-utils/src/mdx/write_deprecations_doc_by_plugin.ts @@ -48,7 +48,8 @@ export async function writeDeprecationDocByPlugin( api.parentPluginId )}" section="${api.id}" text="${api.label}"/>`; - const firstTen = refs.splice(0, 10); + const firstTen = refs.slice(0, 10); + const remainingCount = refs.length - 10; const referencedLocations = firstTen .map( @@ -59,7 +60,7 @@ export async function writeDeprecationDocByPlugin( ref.path }#:~:text=${encodeURIComponent(api.label)})` ) - .join(', ') + (refs.length > 0 ? `+ ${refs.length} more` : ''); + .join(', ') + (remainingCount > 0 ? `+ ${remainingCount} more` : ''); const removeBy = api.removeBy ? api.removeBy : '-'; diff --git a/packages/kbn-docs-utils/src/mdx/write_deprecations_due_by_team.test.ts b/packages/kbn-docs-utils/src/mdx/write_deprecations_due_by_team.test.ts new file mode 100644 index 0000000000000..303c9198244b7 --- /dev/null +++ b/packages/kbn-docs-utils/src/mdx/write_deprecations_due_by_team.test.ts @@ -0,0 +1,344 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import Fsp from 'fs/promises'; +import { ToolingLog } from '@kbn/tooling-log'; +import { writeDeprecationDueByTeam } from './write_deprecations_due_by_team'; +import type { + ReferencedDeprecationsByPlugin, + PluginOrPackage, + ApiDeclaration, + ApiReference, +} from '../types'; +import { + createMockApiDeclaration, + createMockReference, + createMockPlugin, +} from '../__test_helpers__/mocks'; + +jest.mock('fs/promises'); + +const mockFsp = Fsp as jest.Mocked; + +const log = new ToolingLog({ + level: 'debug', + writeTo: { write: () => {} }, +}); + +describe('writeDeprecationDueByTeam', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockFsp.writeFile.mockResolvedValue(undefined); + }); + + it('writes deprecations_by_team.mdx file', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + testPlugin: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'deprecatedFn', + removeBy: '9.0.0', + }), + ref: createMockReference(), + }, + ], + }; + const plugins = [createMockPlugin()]; + + await writeDeprecationDueByTeam('/output', deprecationsByPlugin, plugins, log); + + expect(mockFsp.writeFile).toHaveBeenCalledTimes(1); + const [filePath] = mockFsp.writeFile.mock.calls[0]; + expect(filePath).toContain('deprecations_by_team.mdx'); + }); + + it('groups deprecations by team', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'deprecatedA', + removeBy: '9.0.0', + }), + ref: createMockReference(), + }, + ], + pluginB: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api2', + label: 'deprecatedB', + removeBy: '9.0.0', + }), + ref: createMockReference(), + }, + ], + }; + const plugins = [ + createMockPlugin({ + id: 'pluginA', + manifest: { + id: 'pluginA', + owner: { name: 'Team Alpha' }, + serviceFolders: [], + }, + }), + createMockPlugin({ + id: 'pluginB', + manifest: { + id: 'pluginB', + owner: { name: 'Team Alpha' }, + serviceFolders: [], + }, + }), + ]; + + await writeDeprecationDueByTeam('/output', deprecationsByPlugin, plugins, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('## Team Alpha'); + expect(content).toContain('deprecatedA'); + expect(content).toContain('deprecatedB'); + }); + + it('only includes deprecations with removeBy date', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + testPlugin: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'withDate', + removeBy: '9.0.0', + }), + ref: createMockReference(), + }, + { + deprecatedApi: createMockApiDeclaration({ + id: 'api2', + label: 'withoutDate', + removeBy: undefined, + }), + ref: createMockReference(), + }, + ], + }; + const plugins = [createMockPlugin()]; + + await writeDeprecationDueByTeam('/output', deprecationsByPlugin, plugins, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('withDate'); + expect(content).not.toContain('withoutDate'); + }); + + it('skips plugins without owner name', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + testPlugin: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'deprecatedFn', + removeBy: '9.0.0', + }), + ref: createMockReference(), + }, + ], + }; + const plugins = [ + createMockPlugin({ + manifest: { + id: 'testPlugin', + owner: { name: '' }, + serviceFolders: [], + }, + }), + ]; + + await writeDeprecationDueByTeam('/output', deprecationsByPlugin, plugins, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).not.toContain('deprecatedFn'); + }); + + it('skips plugins not found in plugins list', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + unknownPlugin: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'unknownApi', + removeBy: '9.0.0', + }), + ref: createMockReference(), + }, + ], + }; + const plugins = [createMockPlugin({ id: 'differentPlugin' })]; + + await writeDeprecationDueByTeam('/output', deprecationsByPlugin, plugins, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).not.toContain('unknownApi'); + }); + + it('sorts teams alphabetically', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + pluginZ: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'zebraApi', + removeBy: '9.0.0', + }), + ref: createMockReference(), + }, + ], + pluginA: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api2', + label: 'alphaApi', + removeBy: '9.0.0', + }), + ref: createMockReference(), + }, + ], + }; + const plugins = [ + createMockPlugin({ + id: 'pluginZ', + manifest: { id: 'pluginZ', owner: { name: 'Zebra Team' }, serviceFolders: [] }, + }), + createMockPlugin({ + id: 'pluginA', + manifest: { id: 'pluginA', owner: { name: 'Alpha Team' }, serviceFolders: [] }, + }), + ]; + + await writeDeprecationDueByTeam('/output', deprecationsByPlugin, plugins, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + const alphaIndex = (content as string).indexOf('## Alpha Team'); + const zebraIndex = (content as string).indexOf('## Zebra Team'); + expect(alphaIndex).toBeLessThan(zebraIndex); + }); + + it('includes table with plugin, API, references, and removeBy', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + testPlugin: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'deprecatedFn', + removeBy: '9.0.0', + }), + ref: createMockReference({ plugin: 'consumerPlugin' }), + }, + ], + }; + const plugins = [createMockPlugin()]; + + await writeDeprecationDueByTeam('/output', deprecationsByPlugin, plugins, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('| Plugin | Deprecated API | Reference location(s) | Remove By |'); + expect(content).toContain('consumerPlugin'); + expect(content).toContain('deprecatedFn'); + expect(content).toContain('9.0.0'); + }); + + it('truncates references to first 10 and shows remaining count', async () => { + const deprecatedApi = createMockApiDeclaration({ + id: 'api1', + label: 'popularApi', + removeBy: '9.0.0', + }); + const refs: Array<{ deprecatedApi: ApiDeclaration; ref: ApiReference }> = []; + for (let i = 0; i < 15; i++) { + refs.push({ + deprecatedApi, + ref: createMockReference({ path: `src/plugins/a/file${i}.ts` }), + }); + } + + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + testPlugin: refs, + }; + const plugins = [createMockPlugin()]; + + await writeDeprecationDueByTeam('/output', deprecationsByPlugin, plugins, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('+ 5 more'); + }); + + it('creates GitHub links for reference locations', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + testPlugin: [ + { + deprecatedApi: createMockApiDeclaration({ + id: 'api1', + label: 'linkedFn', + removeBy: '9.0.0', + }), + ref: createMockReference({ path: 'src/plugins/a/specific_file.ts' }), + }, + ], + }; + const plugins = [createMockPlugin()]; + + await writeDeprecationDueByTeam('/output', deprecationsByPlugin, plugins, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('https://github.com/elastic/kibana/tree/main/'); + expect(content).toContain('specific_file.ts'); + }); + + it('handles empty deprecations', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = {}; + const plugins: PluginOrPackage[] = []; + + await writeDeprecationDueByTeam('/output', deprecationsByPlugin, plugins, log); + + expect(mockFsp.writeFile).toHaveBeenCalled(); + }); + + it('includes frontmatter with correct metadata', async () => { + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = {}; + const plugins: PluginOrPackage[] = []; + + await writeDeprecationDueByTeam('/output', deprecationsByPlugin, plugins, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('id: kibDevDocsDeprecationsDueByTeam'); + expect(content).toContain('slug: /kibana-dev-docs/api-meta/deprecations-due-by-team'); + expect(content).toContain('title: Deprecated APIs due to be removed, by team'); + }); + + it('skips deprecated APIs with no references', async () => { + const deprecatedApi = createMockApiDeclaration({ + id: 'api1', + label: 'noRefsApi', + removeBy: '9.0.0', + }); + // Create a scenario where refs array is empty after grouping. + const deprecationsByPlugin: ReferencedDeprecationsByPlugin = { + testPlugin: [{ deprecatedApi, ref: createMockReference() }], + }; + const plugins = [createMockPlugin()]; + + await writeDeprecationDueByTeam('/output', deprecationsByPlugin, plugins, log); + + // The API should be included since it has at least one reference. + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('noRefsApi'); + }); +}); diff --git a/packages/kbn-docs-utils/src/mdx/write_deprecations_due_by_team.ts b/packages/kbn-docs-utils/src/mdx/write_deprecations_due_by_team.ts index d2b61c66222ff..002b479f7d453 100644 --- a/packages/kbn-docs-utils/src/mdx/write_deprecations_due_by_team.ts +++ b/packages/kbn-docs-utils/src/mdx/write_deprecations_due_by_team.ts @@ -75,7 +75,8 @@ export async function writeDeprecationDueByTeam( api.parentPluginId )}" section="${api.id}" text="${api.label}"/>`; - const firstTen = refs.splice(0, 10); + const firstTen = refs.slice(0, 10); + const remainingCount = refs.length - 10; const referencedLocations = firstTen .map( @@ -86,7 +87,7 @@ export async function writeDeprecationDueByTeam( ref.path }#:~:text=${encodeURIComponent(api.label)})` ) - .join(', ') + (refs.length > 0 ? `+ ${refs.length} more` : ''); + .join(', ') + (remainingCount > 0 ? `+ ${remainingCount} more` : ''); const removeBy = api.removeBy ? api.removeBy : '-'; diff --git a/packages/kbn-docs-utils/src/mdx/write_plugin_directory_doc.test.ts b/packages/kbn-docs-utils/src/mdx/write_plugin_directory_doc.test.ts new file mode 100644 index 0000000000000..be4b1302149bf --- /dev/null +++ b/packages/kbn-docs-utils/src/mdx/write_plugin_directory_doc.test.ts @@ -0,0 +1,300 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import Fsp from 'fs/promises'; +import { ToolingLog } from '@kbn/tooling-log'; +import { writePluginDirectoryDoc } from './write_plugin_directory_doc'; +import { + createMockApiDeclaration, + createMockPluginApi, + createMockPluginMetaInfo, +} from '../__test_helpers__/mocks'; + +jest.mock('fs/promises'); + +const mockFsp = Fsp as jest.Mocked; + +const log = new ToolingLog({ + level: 'debug', + writeTo: { write: () => {} }, +}); + +describe('writePluginDirectoryDoc', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockFsp.writeFile.mockResolvedValue(undefined); + }); + + it('writes plugin_directory.mdx file', async () => { + const pluginApiMap = { testPlugin: createMockPluginApi() }; + const pluginStatsMap = { testPlugin: createMockPluginMetaInfo() }; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + expect(mockFsp.writeFile).toHaveBeenCalledTimes(1); + const [filePath] = mockFsp.writeFile.mock.calls[0]; + expect(filePath).toContain('plugin_directory.mdx'); + }); + + it('includes overall stats section', async () => { + const pluginApiMap = { + pluginA: createMockPluginApi({ client: [createMockApiDeclaration()] }), + pluginB: createMockPluginApi(), + }; + const pluginStatsMap = { + pluginA: createMockPluginMetaInfo({ apiCount: 5 }), + pluginB: createMockPluginMetaInfo({ apiCount: 3 }), + }; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('### Overall stats'); + expect(content).toContain( + '| Count | Plugins or Packages with a
public API | Number of teams |' + ); + }); + + it('includes public API health stats section', async () => { + const pluginApiMap = { testPlugin: createMockPluginApi() }; + const pluginStatsMap = { + testPlugin: createMockPluginMetaInfo({ + apiCount: 10, + isAnyType: [createMockApiDeclaration()], + missingComments: [createMockApiDeclaration(), createMockApiDeclaration()], + missingExports: 3, + }), + }; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('### Public API health stats'); + expect(content).toContain('| API Count | Any Count | Missing comments | Missing exports |'); + }); + + it('includes plugin directory section', async () => { + const pluginApiMap = { + testPlugin: createMockPluginApi({ client: [createMockApiDeclaration()] }), + }; + const pluginStatsMap = { + testPlugin: createMockPluginMetaInfo({ isPlugin: true }), + }; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('## Plugin Directory'); + }); + + it('includes package directory section', async () => { + const pluginApiMap = { + '@kbn/test-package': createMockPluginApi({ + id: '@kbn/test-package', + client: [createMockApiDeclaration()], + }), + }; + const pluginStatsMap = { + '@kbn/test-package': createMockPluginMetaInfo({ isPlugin: false }), + }; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('## Package Directory'); + }); + + it('counts unique teams', async () => { + const pluginApiMap = { + pluginA: createMockPluginApi(), + pluginB: createMockPluginApi(), + pluginC: createMockPluginApi(), + }; + const pluginStatsMap = { + pluginA: createMockPluginMetaInfo({ owner: { name: 'Team Alpha' } }), + pluginB: createMockPluginMetaInfo({ owner: { name: 'Team Alpha' } }), + pluginC: createMockPluginMetaInfo({ owner: { name: 'Team Beta' } }), + }; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + // Two unique teams. + expect(content).toContain('| 3 |'); + }); + + it('counts plugins with public API', async () => { + const pluginApiMap = { + pluginWithApi: createMockPluginApi({ client: [createMockApiDeclaration()] }), + pluginWithoutApi: createMockPluginApi({ client: [], server: [], common: [] }), + }; + const pluginStatsMap = { + pluginWithApi: createMockPluginMetaInfo(), + pluginWithoutApi: createMockPluginMetaInfo(), + }; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + // 2 total, 1 with public API. + expect(content).toContain('| 2 | 1 |'); + }); + + it('includes DocLinks for plugins with public API', async () => { + const pluginApiMap = { + testPlugin: createMockPluginApi({ client: [createMockApiDeclaration()] }), + }; + const pluginStatsMap = { + testPlugin: createMockPluginMetaInfo({ isPlugin: true }), + }; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain(' { + const pluginApiMap = { + noApiPlugin: createMockPluginApi({ id: 'noApiPlugin', client: [], server: [], common: [] }), + }; + const pluginStatsMap = { + noApiPlugin: createMockPluginMetaInfo({ isPlugin: true }), + }; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('noApiPlugin'); + expect(content).not.toContain(' { + const pluginApiMap = { + testPlugin: createMockPluginApi({ client: [createMockApiDeclaration()] }), + }; + const pluginStatsMap = { + testPlugin: createMockPluginMetaInfo({ + isPlugin: true, + owner: { name: 'Test Team', githubTeam: 'test-team' }, + }), + }; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('[Test Team](https://github.com/orgs/elastic/teams/test-team)'); + }); + + it('shows team name without link when githubTeam is not available', async () => { + const pluginApiMap = { + testPlugin: createMockPluginApi({ client: [createMockApiDeclaration()] }), + }; + const pluginStatsMap = { + testPlugin: createMockPluginMetaInfo({ + isPlugin: true, + owner: { name: 'Test Team' }, + }), + }; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('Test Team'); + expect(content).not.toContain('https://github.com/orgs/elastic/teams/'); + }); + + it('shows dash for plugins without description', async () => { + const pluginApiMap = { + testPlugin: createMockPluginApi({ client: [createMockApiDeclaration()] }), + }; + const pluginStatsMap = { + testPlugin: createMockPluginMetaInfo({ + isPlugin: true, + description: undefined, + }), + }; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('| - |'); + }); + + it('sorts plugins alphabetically', async () => { + const pluginApiMap = { + zebra: createMockPluginApi({ id: 'zebra', client: [createMockApiDeclaration()] }), + alpha: createMockPluginApi({ id: 'alpha', client: [createMockApiDeclaration()] }), + }; + const pluginStatsMap = { + zebra: createMockPluginMetaInfo({ isPlugin: true }), + alpha: createMockPluginMetaInfo({ isPlugin: true }), + }; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + const alphaIndex = (content as string).indexOf('text="alpha"'); + const zebraIndex = (content as string).indexOf('text="zebra"'); + expect(alphaIndex).toBeLessThan(zebraIndex); + }); + + it('excludes packages without public API from package directory', async () => { + const pluginApiMap = { + '@kbn/no-api': createMockPluginApi({ id: '@kbn/no-api', client: [], server: [], common: [] }), + }; + const pluginStatsMap = { + '@kbn/no-api': createMockPluginMetaInfo({ isPlugin: false }), + }; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + // Package without API should not appear in the package table. + const packageSection = (content as string).split('## Package Directory')[1]; + expect(packageSection).not.toContain('@kbn/no-api'); + }); + + it('includes frontmatter with correct metadata', async () => { + const pluginApiMap = {}; + const pluginStatsMap = {}; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + expect(content).toContain('id: kibDevDocsPluginDirectory'); + expect(content).toContain('slug: /kibana-dev-docs/api-meta/plugin-api-directory'); + expect(content).toContain('title: Directory'); + }); + + it('includes stats for each plugin in the table', async () => { + const pluginApiMap = { + testPlugin: createMockPluginApi({ client: [createMockApiDeclaration()] }), + }; + const pluginStatsMap = { + testPlugin: createMockPluginMetaInfo({ + isPlugin: true, + apiCount: 25, + isAnyType: [createMockApiDeclaration()], + missingComments: [createMockApiDeclaration(), createMockApiDeclaration()], + missingExports: 3, + }), + }; + + await writePluginDirectoryDoc('/output', pluginApiMap, pluginStatsMap, log); + + const [, content] = mockFsp.writeFile.mock.calls[0]; + // The table row should contain stats. + expect(content).toContain('| 25 |'); + expect(content).toContain('| 1 |'); + expect(content).toContain('| 2 |'); + expect(content).toContain('| 3 |'); + }); +}); diff --git a/packages/kbn-docs-utils/src/mdx/write_plugin_mdx_docs.test.ts b/packages/kbn-docs-utils/src/mdx/write_plugin_mdx_docs.test.ts new file mode 100644 index 0000000000000..9d2844e91dd5c --- /dev/null +++ b/packages/kbn-docs-utils/src/mdx/write_plugin_mdx_docs.test.ts @@ -0,0 +1,353 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import Fsp from 'fs/promises'; +import { ToolingLog } from '@kbn/tooling-log'; +import { writePluginDocs, writePluginDoc } from './write_plugin_mdx_docs'; +import { TypeKind } from '../types'; +import { + createMockApiDeclaration, + createMockPlugin, + createMockPluginStats, + createMockPluginApi, +} from '../__test_helpers__/mocks'; + +jest.mock('fs/promises'); +jest.mock('./write_plugin_split_by_folder', () => ({ + writePluginDocSplitByFolder: jest.fn().mockResolvedValue(undefined), +})); + +const mockFsp = Fsp as jest.Mocked; + +const log = new ToolingLog({ + level: 'debug', + writeTo: { write: () => {} }, +}); + +describe('writePluginMdxDocs', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockFsp.writeFile.mockResolvedValue(undefined); + }); + + describe('writePluginDocs', () => { + it('calls writePluginDocSplitByFolder when serviceFolders is defined', async () => { + const { writePluginDocSplitByFolder } = await import('./write_plugin_split_by_folder'); + + const doc = createMockPluginApi({ + serviceFolders: ['service_a'], + client: [createMockApiDeclaration()], + }); + + await writePluginDocs('/output', { + doc, + plugin: createMockPlugin(), + pluginStats: createMockPluginStats(), + log, + }); + + expect(writePluginDocSplitByFolder).toHaveBeenCalledWith('/output', expect.any(Object)); + }); + + it('calls writePluginDoc when serviceFolders is not defined', async () => { + const doc = createMockPluginApi({ + client: [createMockApiDeclaration()], + }); + + await writePluginDocs('/output', { + doc, + plugin: createMockPlugin(), + pluginStats: createMockPluginStats(), + log, + }); + + expect(mockFsp.writeFile).toHaveBeenCalled(); + }); + }); + + describe('writePluginDoc', () => { + it('skips plugins without public API', async () => { + const doc = createMockPluginApi({ + client: [], + server: [], + common: [], + }); + + await writePluginDoc('/output', { + doc, + plugin: createMockPlugin(), + pluginStats: createMockPluginStats(), + log, + }); + + expect(mockFsp.writeFile).not.toHaveBeenCalled(); + }); + + it('writes mdx file for plugin with client API', async () => { + const doc = createMockPluginApi({ + client: [createMockApiDeclaration()], + }); + + await writePluginDoc('/output', { + doc, + plugin: createMockPlugin(), + pluginStats: createMockPluginStats(), + log, + }); + + expect(mockFsp.writeFile).toHaveBeenCalledTimes(2); + const [jsonPath, jsonContent] = mockFsp.writeFile.mock.calls[0]; + expect(jsonPath).toContain('.devdocs.json'); + expect(JSON.parse(jsonContent as string)).toBeDefined(); + + const [mdxPath, mdxContent] = mockFsp.writeFile.mock.calls[1]; + expect(mdxPath).toContain('.mdx'); + expect(mdxContent).toContain('## Client'); + }); + + it('writes mdx file for plugin with server API', async () => { + const doc = createMockPluginApi({ + server: [createMockApiDeclaration()], + }); + + await writePluginDoc('/output', { + doc, + plugin: createMockPlugin(), + pluginStats: createMockPluginStats(), + log, + }); + + const [, mdxContent] = mockFsp.writeFile.mock.calls[1]; + expect(mdxContent).toContain('## Server'); + }); + + it('writes mdx file for plugin with common API', async () => { + const doc = createMockPluginApi({ + common: [createMockApiDeclaration()], + }); + + await writePluginDoc('/output', { + doc, + plugin: createMockPlugin(), + pluginStats: createMockPluginStats(), + log, + }); + + const [, mdxContent] = mockFsp.writeFile.mock.calls[1]; + expect(mdxContent).toContain('## Common'); + }); + + it('includes team contact info when githubTeam is available', async () => { + const doc = createMockPluginApi({ + client: [createMockApiDeclaration()], + }); + + await writePluginDoc('/output', { + doc, + plugin: createMockPlugin({ + manifest: { + id: 'testPlugin', + description: 'A test plugin', + owner: { name: 'Test Team', githubTeam: 'test-team' }, + serviceFolders: [], + }, + }), + pluginStats: createMockPluginStats(), + log, + }); + + const [, mdxContent] = mockFsp.writeFile.mock.calls[1]; + expect(mdxContent).toContain('Contact [Test Team]'); + expect(mdxContent).toContain('https://github.com/orgs/elastic/teams/test-team'); + }); + + it('includes team name without link when githubTeam is not available', async () => { + const doc = createMockPluginApi({ + client: [createMockApiDeclaration()], + }); + + await writePluginDoc('/output', { + doc, + plugin: createMockPlugin({ + manifest: { + id: 'testPlugin', + description: 'A test plugin', + owner: { name: 'Test Team' }, + serviceFolders: [], + }, + }), + pluginStats: createMockPluginStats(), + log, + }); + + const [, mdxContent] = mockFsp.writeFile.mock.calls[1]; + expect(mdxContent).toContain('Contact Test Team'); + expect(mdxContent).not.toContain('https://github.com/orgs/elastic/teams/'); + }); + + it('includes code health stats in the mdx output', async () => { + const doc = createMockPluginApi({ + client: [createMockApiDeclaration()], + }); + + await writePluginDoc('/output', { + doc, + plugin: createMockPlugin(), + pluginStats: createMockPluginStats({ + apiCount: 25, + isAnyType: [createMockApiDeclaration()], + missingComments: [createMockApiDeclaration(), createMockApiDeclaration()], + missingExports: 3, + }), + log, + }); + + const [, mdxContent] = mockFsp.writeFile.mock.calls[1]; + expect(mdxContent).toContain('**Code health stats**'); + expect(mdxContent).toContain('| 25 | 1 | 2 | 3 |'); + }); + + it('includes setup section when present', async () => { + const doc = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'setup', + label: 'Setup', + }), + ], + }); + + await writePluginDoc('/output', { + doc, + plugin: createMockPlugin(), + pluginStats: createMockPluginStats(), + log, + }); + + expect(mockFsp.writeFile).toHaveBeenCalled(); + }); + + it('includes interfaces section when present', async () => { + const doc = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + type: TypeKind.InterfaceKind, + }), + ], + }); + + await writePluginDoc('/output', { + doc, + plugin: createMockPlugin(), + pluginStats: createMockPluginStats(), + log, + }); + + const [, mdxContent] = mockFsp.writeFile.mock.calls[1]; + expect(mdxContent).toContain('### Interfaces'); + }); + + it('includes classes section when present', async () => { + const doc = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + type: TypeKind.ClassKind, + }), + ], + }); + + await writePluginDoc('/output', { + doc, + plugin: createMockPlugin(), + pluginStats: createMockPluginStats(), + log, + }); + + const [, mdxContent] = mockFsp.writeFile.mock.calls[1]; + expect(mdxContent).toContain('### Classes'); + }); + + it('includes enums section when present', async () => { + const doc = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + type: TypeKind.EnumKind, + }), + ], + }); + + await writePluginDoc('/output', { + doc, + plugin: createMockPlugin(), + pluginStats: createMockPluginStats(), + log, + }); + + const [, mdxContent] = mockFsp.writeFile.mock.calls[1]; + expect(mdxContent).toContain('### Enums'); + }); + + it('includes objects section when present', async () => { + const doc = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + type: TypeKind.ObjectKind, + }), + ], + }); + + await writePluginDoc('/output', { + doc, + plugin: createMockPlugin(), + pluginStats: createMockPluginStats(), + log, + }); + + const [, mdxContent] = mockFsp.writeFile.mock.calls[1]; + expect(mdxContent).toContain('### Objects'); + }); + + it('includes misc section for uncategorized types', async () => { + const doc = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + type: TypeKind.StringKind, + }), + ], + }); + + await writePluginDoc('/output', { + doc, + plugin: createMockPlugin(), + pluginStats: createMockPluginStats(), + log, + }); + + const [, mdxContent] = mockFsp.writeFile.mock.calls[1]; + expect(mdxContent).toContain('### Consts, variables and types'); + }); + + it('handles @kbn package names correctly', async () => { + const doc = createMockPluginApi({ + id: '@kbn/some-package', + client: [createMockApiDeclaration()], + }); + + await writePluginDoc('/output', { + doc, + plugin: createMockPlugin({ id: '@kbn/some-package' }), + pluginStats: createMockPluginStats(), + log, + }); + + const [jsonPath] = mockFsp.writeFile.mock.calls[0]; + expect(jsonPath).toContain('kbn_some_package'); + }); + }); +}); diff --git a/packages/kbn-docs-utils/src/stats.test.ts b/packages/kbn-docs-utils/src/stats.test.ts new file mode 100644 index 0000000000000..e4285b6a03dcf --- /dev/null +++ b/packages/kbn-docs-utils/src/stats.test.ts @@ -0,0 +1,700 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { + AdoptionTrackedAPIsByPlugin, + ApiDeclaration, + ApiReference, + MissingApiItemMap, + PluginApi, + ReferencedDeprecationsByPlugin, +} from './types'; +import { TypeKind } from './types'; +import { collectApiStatsForPlugin } from './stats'; + +const createMockApiDeclaration = (overrides: Partial = {}): ApiDeclaration => ({ + id: 'test-id', + label: 'test-label', + type: TypeKind.StringKind, + path: 'src/test/file.ts', + parentPluginId: 'test-plugin', + ...overrides, +}); + +const createMockPluginApi = (overrides: Partial = {}): PluginApi => ({ + id: 'test-plugin', + client: [], + server: [], + common: [], + ...overrides, +}); + +describe('collectApiStatsForPlugin', () => { + describe('missing comments detection', () => { + it('flags API declarations without descriptions', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'no-comment', + label: 'noComment', + description: undefined, + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.missingComments).toHaveLength(1); + expect(stats.missingComments[0].id).toBe('no-comment'); + }); + + it('flags API declarations with empty description arrays', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'empty-comment', + label: 'emptyComment', + description: [], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.missingComments).toHaveLength(1); + expect(stats.missingComments[0].id).toBe('empty-comment'); + }); + + it('does not flag API declarations with descriptions', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'with-comment', + label: 'withComment', + description: ['This has a description'], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.missingComments).toHaveLength(0); + }); + + it('recursively checks children for missing comments', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'parent', + label: 'parent', + description: ['Parent has comment'], + children: [ + createMockApiDeclaration({ + id: 'child-no-comment', + label: 'childNoComment', + description: undefined, + }), + createMockApiDeclaration({ + id: 'child-with-comment', + label: 'childWithComment', + description: ['Child has comment'], + }), + ], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.missingComments).toHaveLength(1); + expect(stats.missingComments[0].id).toBe('child-no-comment'); + }); + + it('handles deeply nested children', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'level1', + label: 'level1', + description: ['Level 1'], + children: [ + createMockApiDeclaration({ + id: 'level2', + label: 'level2', + description: ['Level 2'], + children: [ + createMockApiDeclaration({ + id: 'level3-no-comment', + label: 'level3NoComment', + description: undefined, + }), + ], + }), + ], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.missingComments).toHaveLength(1); + expect(stats.missingComments[0].id).toBe('level3-no-comment'); + }); + + it('checks all scopes (client, server, common)', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'client-no-comment', + description: undefined, + }), + ], + server: [ + createMockApiDeclaration({ + id: 'server-no-comment', + description: undefined, + }), + ], + common: [ + createMockApiDeclaration({ + id: 'common-no-comment', + description: undefined, + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.missingComments).toHaveLength(3); + expect(stats.missingComments.map((d) => d.id)).toEqual([ + 'client-no-comment', + 'server-no-comment', + 'common-no-comment', + ]); + }); + + it('ignores node_modules paths', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'node-modules-api', + label: 'nodeModulesApi', + description: undefined, + path: 'node_modules/some-package/index.ts', + }), + createMockApiDeclaration({ + id: 'regular-api', + label: 'regularApi', + description: undefined, + path: 'src/plugin/file.ts', + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + // Should only flag the regular API, not the node_modules one + expect(stats.missingComments).toHaveLength(1); + expect(stats.missingComments[0].id).toBe('regular-api'); + }); + }); + + describe('any type detection', () => { + it('flags API declarations with AnyKind type', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'any-type', + label: 'anyType', + type: TypeKind.AnyKind, + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.isAnyType).toHaveLength(1); + expect(stats.isAnyType[0].id).toBe('any-type'); + }); + + it('does not flag non-any types', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'string-type', + type: TypeKind.StringKind, + }), + createMockApiDeclaration({ + id: 'number-type', + type: TypeKind.NumberKind, + }), + createMockApiDeclaration({ + id: 'unknown-type', + type: TypeKind.UnknownKind, + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.isAnyType).toHaveLength(0); + }); + + it('recursively checks children for any types', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'parent', + type: TypeKind.ObjectKind, + children: [ + createMockApiDeclaration({ + id: 'child-any', + type: TypeKind.AnyKind, + }), + ], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.isAnyType).toHaveLength(1); + expect(stats.isAnyType[0].id).toBe('child-any'); + }); + + it('checks all scopes for any types', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'client-any', + type: TypeKind.AnyKind, + }), + ], + server: [ + createMockApiDeclaration({ + id: 'server-any', + type: TypeKind.AnyKind, + }), + ], + common: [ + createMockApiDeclaration({ + id: 'common-any', + type: TypeKind.AnyKind, + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.isAnyType).toHaveLength(3); + }); + }); + + describe('missing exports detection', () => { + it('counts missing exports from missingApiItems', () => { + const missingApiItems: MissingApiItemMap = { + 'test-plugin': { + 'missing-type-1': ['ref1', 'ref2'], + 'missing-type-2': ['ref3'], + }, + }; + + const pluginApi = createMockPluginApi(); + + const stats = collectApiStatsForPlugin(pluginApi, missingApiItems, {}, {}); + + expect(stats.missingExports).toBe(2); + }); + + it('handles empty missingApiItems', () => { + const pluginApi = createMockPluginApi(); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.missingExports).toBe(0); + }); + + it('handles missing plugin entry in missingApiItems', () => { + const missingApiItems: MissingApiItemMap = { + 'other-plugin': { + 'missing-type': ['ref1'], + }, + }; + + const pluginApi = createMockPluginApi(); + + const stats = collectApiStatsForPlugin(pluginApi, missingApiItems, {}, {}); + + expect(stats.missingExports).toBe(0); + }); + }); + + describe('API counting', () => { + it('counts single API declaration', () => { + const pluginApi = createMockPluginApi({ + client: [createMockApiDeclaration({ id: 'api1' })], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.apiCount).toBe(1); + }); + + it('counts multiple API declarations across scopes', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ id: 'client1' }), + createMockApiDeclaration({ id: 'client2' }), + ], + server: [createMockApiDeclaration({ id: 'server1' })], + common: [createMockApiDeclaration({ id: 'common1' })], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.apiCount).toBe(4); + }); + + it('counts children in API count', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'parent', + children: [ + createMockApiDeclaration({ id: 'child1' }), + createMockApiDeclaration({ id: 'child2' }), + ], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + // Parent + 2 children = 3 + expect(stats.apiCount).toBe(3); + }); + + it('counts deeply nested children', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'level1', + children: [ + createMockApiDeclaration({ + id: 'level2', + children: [createMockApiDeclaration({ id: 'level3' })], + }), + ], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + // level1 + level2 + level3 = 3 + expect(stats.apiCount).toBe(3); + }); + }); + + describe('no references detection', () => { + it('flags API declarations without references', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'no-refs', + references: undefined, + }), + createMockApiDeclaration({ + id: 'empty-refs', + references: [], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.noReferences).toHaveLength(2); + expect(stats.noReferences.map((d) => d.id)).toEqual(['no-refs', 'empty-refs']); + }); + + it('does not flag API declarations with references', () => { + const references: ApiReference[] = [{ plugin: 'other-plugin', path: 'src/other/file.ts' }]; + + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'with-refs', + references, + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.noReferences).toHaveLength(0); + }); + + it('recursively checks children for references', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'parent', + references: [{ plugin: 'other', path: 'file.ts' }], + children: [ + createMockApiDeclaration({ + id: 'child-no-refs', + references: undefined, + }), + ], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.noReferences).toHaveLength(1); + expect(stats.noReferences[0].id).toBe('child-no-refs'); + }); + }); + + describe('deprecation tracking', () => { + it('counts referenced deprecations', () => { + const deprecations: ReferencedDeprecationsByPlugin = { + 'test-plugin': [ + { + deprecatedApi: createMockApiDeclaration({ id: 'deprecated1' }), + ref: { plugin: 'other-plugin', path: 'file.ts' }, + }, + { + deprecatedApi: createMockApiDeclaration({ id: 'deprecated2' }), + ref: { plugin: 'other-plugin', path: 'file2.ts' }, + }, + ], + }; + + const pluginApi = createMockPluginApi(); + + const stats = collectApiStatsForPlugin(pluginApi, {}, deprecations, {}); + + expect(stats.deprecatedAPIsReferencedCount).toBe(2); + }); + + it('handles no deprecations', () => { + const pluginApi = createMockPluginApi(); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.deprecatedAPIsReferencedCount).toBe(0); + }); + + it('handles missing plugin entry in deprecations', () => { + const deprecations: ReferencedDeprecationsByPlugin = { + 'other-plugin': [ + { + deprecatedApi: createMockApiDeclaration({ id: 'deprecated' }), + ref: { plugin: 'other', path: 'file.ts' }, + }, + ], + }; + + const pluginApi = createMockPluginApi(); + + const stats = collectApiStatsForPlugin(pluginApi, {}, deprecations, {}); + + expect(stats.deprecatedAPIsReferencedCount).toBe(0); + }); + }); + + describe('adoption tracking', () => { + it('tracks adoption-tracked APIs', () => { + const adoptionTrackedAPIs: AdoptionTrackedAPIsByPlugin = { + 'test-plugin': [ + { + trackedApi: { id: 'api1', label: 'API1' }, + references: ['plugin1', 'plugin2'], + }, + { + trackedApi: { id: 'api2', label: 'API2' }, + references: [], + }, + ], + }; + + const pluginApi = createMockPluginApi(); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, adoptionTrackedAPIs); + + expect(stats.adoptionTrackedAPIs).toHaveLength(2); + expect(stats.adoptionTrackedAPIsCount).toBe(2); + expect(stats.adoptionTrackedAPIsUnreferencedCount).toBe(1); + }); + + it('handles no adoption-tracked APIs', () => { + const pluginApi = createMockPluginApi(); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.adoptionTrackedAPIs).toHaveLength(0); + expect(stats.adoptionTrackedAPIsCount).toBe(0); + expect(stats.adoptionTrackedAPIsUnreferencedCount).toBe(0); + }); + + it('handles missing plugin entry in adoptionTrackedAPIs', () => { + const adoptionTrackedAPIs: AdoptionTrackedAPIsByPlugin = { + 'other-plugin': [ + { + trackedApi: { id: 'api1', label: 'API1' }, + references: ['plugin1'], + }, + ], + }; + + const pluginApi = createMockPluginApi(); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, adoptionTrackedAPIs); + + expect(stats.adoptionTrackedAPIs).toHaveLength(0); + expect(stats.adoptionTrackedAPIsCount).toBe(0); + }); + }); + + describe('destructured parameter handling', () => { + it('flags missing comments on destructured parameter children', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'function-with-destructured-params', + label: 'functionWithDestructuredParams', + type: TypeKind.FunctionKind, + description: ['Function description'], + children: [ + createMockApiDeclaration({ + id: 'param-obj', + label: 'obj', + description: ['Parent param has comment'], + children: [ + createMockApiDeclaration({ + id: 'param-obj-prop', + label: 'prop', + description: undefined, // Missing property-level comment + }), + ], + }), + ], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + // Should flag the child property as missing comment + // Note: This is current behavior - in Phase 4 we'll fix this to check for property-level JSDoc + expect(stats.missingComments).toHaveLength(1); + expect(stats.missingComments[0].id).toBe('param-obj-prop'); + }); + + it('handles nested destructured parameters', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'nested-destructured', + type: TypeKind.FunctionKind, + description: ['Function has comment'], + children: [ + createMockApiDeclaration({ + id: 'level1', + description: ['Level 1 has comment'], + children: [ + createMockApiDeclaration({ + id: 'level2', + description: ['Level 2 has comment'], + children: [ + createMockApiDeclaration({ + id: 'level3-no-comment', + description: undefined, + }), + ], + }), + ], + }), + ], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, {}, {}, {}); + + expect(stats.missingComments).toHaveLength(1); + expect(stats.missingComments[0].id).toBe('level3-no-comment'); + }); + }); + + describe('combined scenarios', () => { + it('handles complex plugin API with multiple issues', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'api-with-any', + type: TypeKind.AnyKind, + description: ['Has description'], + references: [{ plugin: 'other', path: 'file.ts' }], + }), + createMockApiDeclaration({ + id: 'api-no-comment', + description: undefined, + references: [{ plugin: 'other', path: 'file.ts' }], + }), + createMockApiDeclaration({ + id: 'api-no-refs', + description: ['Has description but no refs'], + references: [], + }), + createMockApiDeclaration({ + id: 'api-good', + description: ['Good API'], + references: [{ plugin: 'other', path: 'file.ts' }], + }), + ], + server: [ + createMockApiDeclaration({ + id: 'server-any', + type: TypeKind.AnyKind, + description: ['Server has any type but has description'], + references: [{ plugin: 'other', path: 'file.ts' }], + }), + ], + }); + + const missingApiItems: MissingApiItemMap = { + 'test-plugin': { + 'missing-1': ['ref1'], + 'missing-2': ['ref2'], + }, + }; + + const deprecations: ReferencedDeprecationsByPlugin = { + 'test-plugin': [ + { + deprecatedApi: createMockApiDeclaration({ id: 'deprecated' }), + ref: { plugin: 'other', path: 'file.ts' }, + }, + ], + }; + + const stats = collectApiStatsForPlugin(pluginApi, missingApiItems, deprecations, {}); + + expect(stats.apiCount).toBe(5); + expect(stats.missingComments).toHaveLength(1); + expect(stats.isAnyType).toHaveLength(2); + expect(stats.noReferences).toHaveLength(1); + expect(stats.missingExports).toBe(2); + expect(stats.deprecatedAPIsReferencedCount).toBe(1); + }); + }); +}); diff --git a/packages/kbn-docs-utils/src/trim_deleted_docs_from_nav.test.ts b/packages/kbn-docs-utils/src/trim_deleted_docs_from_nav.test.ts new file mode 100644 index 0000000000000..9810b53fef433 --- /dev/null +++ b/packages/kbn-docs-utils/src/trim_deleted_docs_from_nav.test.ts @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import Fsp from 'fs/promises'; +import Path from 'path'; +import { ToolingLog } from '@kbn/tooling-log'; +import { trimDeletedDocsFromNav } from './trim_deleted_docs_from_nav'; + +// Mock fs/promises +jest.mock('fs/promises'); + +// Mock getAllDocFileIds +jest.mock('./mdx/get_all_doc_file_ids', () => ({ + getAllDocFileIds: jest.fn(() => Promise.resolve(['doc1', 'doc2', 'doc3'])), +})); + +const log = new ToolingLog({ + level: 'silent', + writeTo: process.stdout, +}); + +describe('trimDeletedDocsFromNav', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('removes deleted doc IDs from nav', async () => { + const initialDocIds = ['doc1', 'doc2', 'doc3', 'doc4', 'doc5']; + const outputDir = Path.resolve(__dirname, 'test_output'); + + const mockNav = { + items: [ + { id: 'doc1', title: 'Doc 1' }, + { id: 'doc2', title: 'Doc 2' }, + { + id: 'parent', + items: [ + { id: 'doc3', title: 'Doc 3' }, + { id: 'doc4', title: 'Doc 4' }, // This should be removed + { id: 'doc5', title: 'Doc 5' }, // This should be removed + ], + }, + ], + }; + + (Fsp.readFile as jest.Mock).mockResolvedValue(JSON.stringify(mockNav)); + (Fsp.writeFile as jest.Mock).mockResolvedValue(undefined); + + await trimDeletedDocsFromNav(log, initialDocIds, outputDir); + + // Should have called writeFile to update nav + expect(Fsp.writeFile).toHaveBeenCalled(); + const writeCall = (Fsp.writeFile as jest.Mock).mock.calls[0]; + const updatedNav = JSON.parse(writeCall[1]); + + // doc4 and doc5 should be removed + const parentItem = updatedNav.items.find((item: any) => item.id === 'parent'); + expect(parentItem).toBeDefined(); + expect(parentItem.items).toHaveLength(1); + expect(parentItem.items[0].id).toBe('doc3'); + }); + + it('does not update nav when no docs are deleted', async () => { + const initialDocIds = ['doc1', 'doc2', 'doc3']; + const outputDir = Path.resolve(__dirname, 'test_output'); + + const mockNav = { + items: [ + { id: 'doc1', title: 'Doc 1' }, + { id: 'doc2', title: 'Doc 2' }, + { id: 'doc3', title: 'Doc 3' }, + ], + }; + + (Fsp.readFile as jest.Mock).mockResolvedValue(JSON.stringify(mockNav)); + + await trimDeletedDocsFromNav(log, initialDocIds, outputDir); + + // Should not call writeFile when no docs are deleted + expect(Fsp.writeFile).not.toHaveBeenCalled(); + }); + + it('handles nav file read errors', async () => { + const initialDocIds = ['doc1']; + const outputDir = Path.resolve(__dirname, 'test_output'); + + (Fsp.readFile as jest.Mock).mockRejectedValue(new Error('File not found')); + + await expect(trimDeletedDocsFromNav(log, initialDocIds, outputDir)).rejects.toThrow( + 'unable to read dev-docs nav' + ); + }); + + it('handles invalid JSON in nav file', async () => { + const initialDocIds = ['doc1']; + const outputDir = Path.resolve(__dirname, 'test_output'); + + (Fsp.readFile as jest.Mock).mockResolvedValue('invalid json {'); + + await expect(trimDeletedDocsFromNav(log, initialDocIds, outputDir)).rejects.toThrow( + 'unable to parse nav' + ); + }); + + it('preserves newline at end of nav file', async () => { + const initialDocIds = ['doc1', 'doc2', 'doc3', 'doc4']; + const outputDir = Path.resolve(__dirname, 'test_output'); + + const mockNav = { + items: [ + { id: 'doc1' }, + { id: 'doc4' }, // Should be removed + ], + }; + + (Fsp.readFile as jest.Mock).mockResolvedValue(JSON.stringify(mockNav) + '\n'); + (Fsp.writeFile as jest.Mock).mockResolvedValue(undefined); + + await trimDeletedDocsFromNav(log, initialDocIds, outputDir); + + expect(Fsp.writeFile).toHaveBeenCalled(); + const writeCall = (Fsp.writeFile as jest.Mock).mock.calls[0]; + expect(writeCall[1].endsWith('\n')).toBe(true); + }); + + it('handles nested nav structures', async () => { + const initialDocIds = ['doc1', 'doc2', 'doc3', 'doc4']; + const outputDir = Path.resolve(__dirname, 'test_output'); + + const mockNav = { + items: [ + { + id: 'level1', + items: [ + { + id: 'level2', + items: [ + { id: 'doc1' }, + { id: 'doc4' }, // Should be removed + ], + }, + ], + }, + ], + }; + + (Fsp.readFile as jest.Mock).mockResolvedValue(JSON.stringify(mockNav)); + (Fsp.writeFile as jest.Mock).mockResolvedValue(undefined); + + await trimDeletedDocsFromNav(log, initialDocIds, outputDir); + + expect(Fsp.writeFile).toHaveBeenCalled(); + const writeCall = (Fsp.writeFile as jest.Mock).mock.calls[0]; + const updatedNav = JSON.parse(writeCall[1]); + + // doc4 should be removed from nested structure + expect(updatedNav.items[0].items[0].items).toHaveLength(1); + expect(updatedNav.items[0].items[0].items[0].id).toBe('doc1'); + }); +}); diff --git a/packages/kbn-docs-utils/src/types.ts b/packages/kbn-docs-utils/src/types.ts index 020a96cf75586..e693f0b615ef0 100644 --- a/packages/kbn-docs-utils/src/types.ts +++ b/packages/kbn-docs-utils/src/types.ts @@ -186,6 +186,16 @@ export interface ApiDeclaration { */ path: string; + /** + * Source position for easier navigation to the declaration. + */ + lineNumber?: number; + + /** + * Source position for easier navigation to the declaration. + */ + columnNumber?: number; + /** * Other plugins that reference this API item (along with SourceLink info for each reference). */ diff --git a/packages/kbn-docs-utils/src/utils.test.ts b/packages/kbn-docs-utils/src/utils.test.ts index 7fea32e33b8e9..706e8cdaf8959 100644 --- a/packages/kbn-docs-utils/src/utils.test.ts +++ b/packages/kbn-docs-utils/src/utils.test.ts @@ -14,8 +14,23 @@ import { REPO_ROOT } from '@kbn/repo-info'; import { findPlugins } from './find_plugins'; import { getPluginApi } from './get_plugin_api'; import { getKibanaPlatformPlugin } from './integration_tests/kibana_platform_plugin_mock'; -import type { PluginApi, PluginOrPackage } from './types'; -import { getPluginForPath, getServiceForPath, removeBrokenLinks, getFileName } from './utils'; +import type { PluginApi, PluginOrPackage, ScopeApi, ApiDeclaration } from './types'; +import { TypeKind, Lifecycle, ApiScope } from './types'; +import { + getPluginForPath, + getServiceForPath, + removeBrokenLinks, + getFileName, + getPluginApiDocId, + groupPluginApi, + addApiDeclarationToScope, + countScopeApi, + createEmptyScope, + getSlug, + getApiSectionId, + isInternal, +} from './utils'; +import { createMockApiDeclaration } from './__test_helpers__/mocks'; const log = new ToolingLog({ level: 'debug', @@ -95,3 +110,374 @@ it('test removeBrokenLinks', () => { 0 ); }); + +describe('getServiceForPath edge cases', () => { + it('handles common folder paths', () => { + expect(getServiceForPath('src/plugins/embed/common/service/file.ts', 'src/plugins/embed')).toBe( + 'service' + ); + expect( + getServiceForPath('src/plugins/embed/common/another_service/index.ts', 'src/plugins/embed') + ).toBe('another_service'); + }); + + it('handles paths with special characters in directory name', () => { + const pluginDir = 'src/platform/plugins/shared/data'; + const path = `${pluginDir}/public/search_services/file.ts`; + expect(getServiceForPath(path, pluginDir)).toBe('search_services'); + }); + + it('returns undefined for paths without service folders', () => { + expect( + getServiceForPath('src/plugins/embed/public/file.ts', 'src/plugins/embed') + ).toBeUndefined(); + expect( + getServiceForPath('src/plugins/embed/server/file.ts', 'src/plugins/embed') + ).toBeUndefined(); + expect( + getServiceForPath('src/plugins/embed/common/file.ts', 'src/plugins/embed') + ).toBeUndefined(); + }); +}); + +describe('getPluginApiDocId', () => { + it('generates doc ID for plugin without service folders', () => { + const id = getPluginApiDocId('pluginA.public.SomeAPI', undefined); + // `snakeToCamel` only converts underscores/dashes followed by lowercase letters, + // so `_S` in `_SomeAPI` remains as-is. + expect(id).toBe('kibPluginAPublic_SomeAPIPluginApi'); + }); + + it('generates doc ID for plugin with service folder', () => { + const id = getPluginApiDocId('pluginA.public.SomeAPI', { + serviceFolders: ['foo'], + apiPath: 'src/plugins/pluginA/public/foo/index.ts', + directory: 'src/plugins/pluginA', + }); + // Service folder `foo` is capitalized and appended. + expect(id).toBe('kibPluginAPublic_SomeAPIFooPluginApi'); + }); + + it('handles @kbn package IDs', () => { + const id = getPluginApiDocId('@kbn/some-package.public.API', undefined); + // The `@` is stripped, dots/slashes become underscores, and `snakeToCamel` applies. + expect(id).toBe('kibKbnSomePackagePublic_APIPluginApi'); + }); + + it('handles complex package paths', () => { + const id = getPluginApiDocId('@kbn/repo-packages.public.API', undefined); + expect(id).toBe('kibKbnRepoPackagesPublic_APIPluginApi'); + }); +}); + +describe('groupPluginApi', () => { + it('groups declarations by type kind', () => { + const declarations: ApiDeclaration[] = [ + createMockApiDeclaration({ id: 'func1', label: 'funcB', type: TypeKind.FunctionKind }), + createMockApiDeclaration({ id: 'func2', label: 'funcA', type: TypeKind.FunctionKind }), + createMockApiDeclaration({ id: 'class1', label: 'MyClass', type: TypeKind.ClassKind }), + createMockApiDeclaration({ + id: 'interface1', + label: 'MyInterface', + type: TypeKind.InterfaceKind, + }), + createMockApiDeclaration({ id: 'enum1', label: 'MyEnum', type: TypeKind.EnumKind }), + createMockApiDeclaration({ id: 'obj1', label: 'myObject', type: TypeKind.ObjectKind }), + createMockApiDeclaration({ id: 'misc1', label: 'miscItem', type: TypeKind.StringKind }), + ]; + + const scope = groupPluginApi(declarations); + + expect(scope.functions).toHaveLength(2); + expect(scope.classes).toHaveLength(1); + expect(scope.interfaces).toHaveLength(1); + expect(scope.enums).toHaveLength(1); + expect(scope.objects).toHaveLength(1); + expect(scope.misc).toHaveLength(1); + }); + + it('sorts each group alphabetically by label', () => { + const declarations: ApiDeclaration[] = [ + createMockApiDeclaration({ id: 'func1', label: 'zebra', type: TypeKind.FunctionKind }), + createMockApiDeclaration({ id: 'func2', label: 'alpha', type: TypeKind.FunctionKind }), + createMockApiDeclaration({ id: 'func3', label: 'middle', type: TypeKind.FunctionKind }), + ]; + + const scope = groupPluginApi(declarations); + + expect(scope.functions[0].label).toBe('alpha'); + expect(scope.functions[1].label).toBe('middle'); + expect(scope.functions[2].label).toBe('zebra'); + }); + + it('handles empty declarations array', () => { + const scope = groupPluginApi([]); + + expect(scope.functions).toHaveLength(0); + expect(scope.classes).toHaveLength(0); + expect(scope.interfaces).toHaveLength(0); + expect(scope.enums).toHaveLength(0); + expect(scope.objects).toHaveLength(0); + expect(scope.misc).toHaveLength(0); + }); + + it('handles setup lifecycle declarations', () => { + const declarations: ApiDeclaration[] = [ + createMockApiDeclaration({ + id: 'setup', + label: 'Setup', + type: TypeKind.InterfaceKind, + lifecycle: Lifecycle.SETUP, + }), + ]; + + const scope = groupPluginApi(declarations); + + expect(scope.setup).toBeDefined(); + expect(scope.setup?.label).toBe('Setup'); + }); + + it('handles start lifecycle declarations', () => { + const declarations: ApiDeclaration[] = [ + createMockApiDeclaration({ + id: 'start', + label: 'Start', + type: TypeKind.InterfaceKind, + lifecycle: Lifecycle.START, + }), + ]; + + const scope = groupPluginApi(declarations); + + expect(scope.start).toBeDefined(); + expect(scope.start?.label).toBe('Start'); + }); +}); + +describe('addApiDeclarationToScope', () => { + it('adds setup lifecycle declaration to scope.setup', () => { + const scope = createEmptyScope(); + const declaration = createMockApiDeclaration({ lifecycle: Lifecycle.SETUP }); + + addApiDeclarationToScope(declaration, scope); + + expect(scope.setup).toBe(declaration); + }); + + it('adds start lifecycle declaration to scope.start', () => { + const scope = createEmptyScope(); + const declaration = createMockApiDeclaration({ lifecycle: Lifecycle.START }); + + addApiDeclarationToScope(declaration, scope); + + expect(scope.start).toBe(declaration); + }); + + it('adds ClassKind to classes array', () => { + const scope = createEmptyScope(); + const declaration = createMockApiDeclaration({ type: TypeKind.ClassKind }); + + addApiDeclarationToScope(declaration, scope); + + expect(scope.classes).toContain(declaration); + }); + + it('adds InterfaceKind to interfaces array', () => { + const scope = createEmptyScope(); + const declaration = createMockApiDeclaration({ type: TypeKind.InterfaceKind }); + + addApiDeclarationToScope(declaration, scope); + + expect(scope.interfaces).toContain(declaration); + }); + + it('adds EnumKind to enums array', () => { + const scope = createEmptyScope(); + const declaration = createMockApiDeclaration({ type: TypeKind.EnumKind }); + + addApiDeclarationToScope(declaration, scope); + + expect(scope.enums).toContain(declaration); + }); + + it('adds FunctionKind to functions array', () => { + const scope = createEmptyScope(); + const declaration = createMockApiDeclaration({ type: TypeKind.FunctionKind }); + + addApiDeclarationToScope(declaration, scope); + + expect(scope.functions).toContain(declaration); + }); + + it('adds ObjectKind to objects array', () => { + const scope = createEmptyScope(); + const declaration = createMockApiDeclaration({ type: TypeKind.ObjectKind }); + + addApiDeclarationToScope(declaration, scope); + + expect(scope.objects).toContain(declaration); + }); + + it('adds uncategorized types to misc array', () => { + const scope = createEmptyScope(); + const declaration = createMockApiDeclaration({ type: TypeKind.StringKind }); + + addApiDeclarationToScope(declaration, scope); + + expect(scope.misc).toContain(declaration); + }); + + it('adds ArrayKind to misc array', () => { + const scope = createEmptyScope(); + const declaration = createMockApiDeclaration({ type: TypeKind.ArrayKind }); + + addApiDeclarationToScope(declaration, scope); + + expect(scope.misc).toContain(declaration); + }); +}); + +describe('countScopeApi', () => { + it('returns 0 for empty scope', () => { + const scope = createEmptyScope(); + + expect(countScopeApi(scope)).toBe(0); + }); + + it('counts setup and start as 1 each', () => { + const scope: ScopeApi = { + ...createEmptyScope(), + setup: createMockApiDeclaration(), + start: createMockApiDeclaration(), + }; + + expect(countScopeApi(scope)).toBe(2); + }); + + it('counts all array items', () => { + const scope: ScopeApi = { + ...createEmptyScope(), + classes: [createMockApiDeclaration(), createMockApiDeclaration()], + interfaces: [createMockApiDeclaration()], + functions: [ + createMockApiDeclaration(), + createMockApiDeclaration(), + createMockApiDeclaration(), + ], + objects: [createMockApiDeclaration()], + enums: [createMockApiDeclaration()], + misc: [createMockApiDeclaration(), createMockApiDeclaration()], + }; + + expect(countScopeApi(scope)).toBe(10); + }); + + it('counts combined setup, start, and arrays', () => { + const scope: ScopeApi = { + setup: createMockApiDeclaration(), + start: createMockApiDeclaration(), + classes: [createMockApiDeclaration()], + interfaces: [createMockApiDeclaration()], + functions: [createMockApiDeclaration()], + objects: [createMockApiDeclaration()], + enums: [createMockApiDeclaration()], + misc: [createMockApiDeclaration()], + }; + + expect(countScopeApi(scope)).toBe(8); + }); +}); + +describe('createEmptyScope', () => { + it('returns scope with empty arrays', () => { + const scope = createEmptyScope(); + + expect(scope.classes).toEqual([]); + expect(scope.functions).toEqual([]); + expect(scope.interfaces).toEqual([]); + expect(scope.enums).toEqual([]); + expect(scope.misc).toEqual([]); + expect(scope.objects).toEqual([]); + }); + + it('returns scope without setup and start', () => { + const scope = createEmptyScope(); + + expect(scope.setup).toBeUndefined(); + expect(scope.start).toBeUndefined(); + }); +}); + +describe('getSlug', () => { + it('removes @ symbol from package names', () => { + expect(getSlug('@kbn/some-package')).toBe('kbn-some-package'); + }); + + it('replaces dots with dashes', () => { + expect(getSlug('plugin.a.b')).toBe('plugin-a-b'); + }); + + it('replaces forward slashes with dashes', () => { + expect(getSlug('plugin/a/b')).toBe('plugin-a-b'); + }); + + it('replaces backslashes with dashes', () => { + expect(getSlug('plugin\\a\\b')).toBe('plugin-a-b'); + }); + + it('handles complex package paths', () => { + expect(getSlug('@kbn/repo-packages.subpackage/path')).toBe('kbn-repo-packages-subpackage-path'); + }); +}); + +describe('getApiSectionId', () => { + it('creates section ID with scope and cleaned name', () => { + const result = getApiSectionId({ id: 'myFunction', scope: ApiScope.CLIENT }); + + expect(result).toBe('def-public.myFunction'); + }); + + it('removes special characters from ID', () => { + const result = getApiSectionId({ id: '{ param1, param2 }', scope: ApiScope.SERVER }); + + expect(result).toBe('def-server.param1param2'); + }); + + it('handles common scope', () => { + const result = getApiSectionId({ id: 'sharedUtil', scope: ApiScope.COMMON }); + + expect(result).toBe('def-common.sharedUtil'); + }); + + it('preserves dots, underscores, and dollar signs', () => { + const result = getApiSectionId({ id: 'my_func.$helper.inner', scope: ApiScope.CLIENT }); + + expect(result).toBe('def-public.my_func.$helper.inner'); + }); +}); + +describe('isInternal', () => { + it('returns true when declaration has internal tag', () => { + const declaration = createMockApiDeclaration({ tags: ['internal', 'deprecated'] }); + + expect(isInternal(declaration)).toBe('internal'); + }); + + it('returns undefined when declaration has no internal tag', () => { + const declaration = createMockApiDeclaration({ tags: ['beta', 'deprecated'] }); + + expect(isInternal(declaration)).toBeUndefined(); + }); + + it('returns undefined when declaration has no tags', () => { + const declaration = createMockApiDeclaration({ tags: undefined }); + + expect(isInternal(declaration)).toBeUndefined(); + }); + + it('returns undefined when tags array is empty', () => { + const declaration = createMockApiDeclaration({ tags: [] }); + + expect(isInternal(declaration)).toBeUndefined(); + }); +}); From 2a74009b6c092244f37c7e8361027488c47560e4 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 31 Dec 2025 21:33:52 +0000 Subject: [PATCH 2/4] Changes from node scripts/eslint_all_files --no-cache --fix --- packages/kbn-docs-utils/src/__test_helpers__/mocks.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kbn-docs-utils/src/__test_helpers__/mocks.ts b/packages/kbn-docs-utils/src/__test_helpers__/mocks.ts index 33af4f7caba9b..129bd4b3dc3b5 100644 --- a/packages/kbn-docs-utils/src/__test_helpers__/mocks.ts +++ b/packages/kbn-docs-utils/src/__test_helpers__/mocks.ts @@ -106,4 +106,3 @@ export const createMockPluginMetaInfo = ( isPlugin: true, ...overrides, }); - From 694c0213508c5422d130e0d7fd37db8f5aae49f6 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Thu, 1 Jan 2026 14:21:13 -0500 Subject: [PATCH 3/4] Fix test --- packages/kbn-docs-utils/src/count_eslint_disable.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-docs-utils/src/count_eslint_disable.test.ts b/packages/kbn-docs-utils/src/count_eslint_disable.test.ts index edb876fa5be72..6bd081a13f049 100644 --- a/packages/kbn-docs-utils/src/count_eslint_disable.test.ts +++ b/packages/kbn-docs-utils/src/count_eslint_disable.test.ts @@ -34,7 +34,7 @@ describe('countEslintDisableLines', () => { expect(counts).toMatchInlineSnapshot(` Object { "eslintDisableFileCount": 3, - "eslintDisableLineCount": 10, + "eslintDisableLineCount": 9, } `); }); From 768d2778b453f922d33bb9e6a91abf2079f89475 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 1 Jan 2026 19:31:53 +0000 Subject: [PATCH 4/4] Changes from node scripts/notice --- NOTICE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NOTICE.txt b/NOTICE.txt index 6bc520539e946..c0646243c49f3 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,5 +1,5 @@ Kibana source code with Kibana X-Pack source code -Copyright 2012-2025 Elasticsearch B.V. +Copyright 2012-2026 Elasticsearch B.V. --- Adapted from remote-web-worker, which was available under a "MIT" license.