diff --git a/packages/kbn-docs-utils/scripts/update_fixture_comments.js b/packages/kbn-docs-utils/scripts/update_fixture_comments.js index d40b83562d082..aadb3c1aafd61 100644 --- a/packages/kbn-docs-utils/scripts/update_fixture_comments.js +++ b/packages/kbn-docs-utils/scripts/update_fixture_comments.js @@ -12,6 +12,7 @@ const path = require('node:path'); const categories = [ { key: 'missingComments', title: 'missing comments' }, + { key: 'paramDocMismatches', title: 'param doc mismatches' }, { key: 'isAnyType', title: 'any usage' }, { key: 'noReferences', title: 'no references' }, ]; diff --git a/packages/kbn-docs-utils/src/README.md b/packages/kbn-docs-utils/src/README.md index ca1b17efee728..6a074254f809a 100644 --- a/packages/kbn-docs-utils/src/README.md +++ b/packages/kbn-docs-utils/src/README.md @@ -1,4 +1,4 @@ -# Autogenerated API documentation +# Auto-generated API documentation. [RFC](https://github.com/elastic/kibana/blob/main/legacy_rfcs/text/0014_api_documentation.md) diff --git a/packages/kbn-docs-utils/src/__test_helpers__/mocks.ts b/packages/kbn-docs-utils/src/__test_helpers__/mocks.ts index 129bd4b3dc3b5..c989b1f6ac0ce 100644 --- a/packages/kbn-docs-utils/src/__test_helpers__/mocks.ts +++ b/packages/kbn-docs-utils/src/__test_helpers__/mocks.ts @@ -65,6 +65,7 @@ export const createMockPluginStats = (overrides: Partial = {}): ApiSta missingComments: [], isAnyType: [], noReferences: [], + paramDocMismatches: [], missingExports: 0, deprecatedAPIsReferencedCount: 0, unreferencedDeprecatedApisCount: 0, @@ -95,6 +96,7 @@ export const createMockPluginMetaInfo = ( missingComments: [], isAnyType: [], noReferences: [], + paramDocMismatches: [], missingExports: 0, deprecatedAPIsReferencedCount: 0, unreferencedDeprecatedApisCount: 0, diff --git a/packages/kbn-docs-utils/src/check_package_docs_cli.test.ts b/packages/kbn-docs-utils/src/check_package_docs_cli.test.ts index 69293b4930a4b..9c75b87c0252c 100644 --- a/packages/kbn-docs-utils/src/check_package_docs_cli.test.ts +++ b/packages/kbn-docs-utils/src/check_package_docs_cli.test.ts @@ -42,6 +42,7 @@ const createBaseStats = (pluginId: string): AllPluginStats => ({ missingComments: [], isAnyType: [], noReferences: [], + paramDocMismatches: [], apiCount: 0, missingExports: 0, deprecatedAPIsReferencedCount: 0, diff --git a/packages/kbn-docs-utils/src/check_package_docs_cli.ts b/packages/kbn-docs-utils/src/check_package_docs_cli.ts index 880ba3ad7e583..ace0049377208 100644 --- a/packages/kbn-docs-utils/src/check_package_docs_cli.ts +++ b/packages/kbn-docs-utils/src/check_package_docs_cli.ts @@ -83,7 +83,9 @@ export const getValidationResults = ( : 0; const hasAnyIssues = shouldCheckAny && pluginStats.isAnyType.length > 0; - const hasCommentIssues = shouldCheckComments && pluginStats.missingComments.length > 0; + const hasCommentIssues = + shouldCheckComments && + (pluginStats.missingComments.length > 0 || pluginStats.paramDocMismatches.length > 0); const hasExportIssues = shouldCheckExports && missingExports > 0; return { diff --git a/packages/kbn-docs-utils/src/cli/tasks/collect_stats.test.ts b/packages/kbn-docs-utils/src/cli/tasks/collect_stats.test.ts index 7e9d5d8c70043..60020dc6527c9 100644 --- a/packages/kbn-docs-utils/src/cli/tasks/collect_stats.test.ts +++ b/packages/kbn-docs-utils/src/cli/tasks/collect_stats.test.ts @@ -77,6 +77,7 @@ describe('collectStats', () => { missingComments: [], isAnyType: [], noReferences: [], + paramDocMismatches: [], missingExports: 0, deprecatedAPIsReferencedCount: 0, unreferencedDeprecatedApisCount: 0, diff --git a/packages/kbn-docs-utils/src/cli/tasks/report_metrics.test.ts b/packages/kbn-docs-utils/src/cli/tasks/report_metrics.test.ts index 31b0f371b5192..80449fd6d990d 100644 --- a/packages/kbn-docs-utils/src/cli/tasks/report_metrics.test.ts +++ b/packages/kbn-docs-utils/src/cli/tasks/report_metrics.test.ts @@ -80,6 +80,7 @@ describe('reportMetrics', () => { missingComments: [], isAnyType: [], noReferences: [], + paramDocMismatches: [], missingExports: 0, deprecatedAPIsReferencedCount: 0, unreferencedDeprecatedApisCount: 0, diff --git a/packages/kbn-docs-utils/src/cli/tasks/write_docs.test.ts b/packages/kbn-docs-utils/src/cli/tasks/write_docs.test.ts index 5d64cb1011037..415515b21cec8 100644 --- a/packages/kbn-docs-utils/src/cli/tasks/write_docs.test.ts +++ b/packages/kbn-docs-utils/src/cli/tasks/write_docs.test.ts @@ -92,6 +92,7 @@ describe('writeDocs', () => { missingComments: [], isAnyType: [], noReferences: [], + paramDocMismatches: [], missingExports: 0, deprecatedAPIsReferencedCount: 0, unreferencedDeprecatedApisCount: 0, 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 60d80516f86d4..9635d3d4febf3 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 @@ -128,6 +128,10 @@ export interface IReturnAReactComponent { // line 71 - CrazyClass // line 94 - foo // line 117 - component +// param doc mismatches (3): +// line 52 - Constructor +// line 91 - anOptionalFn +// line 101 - fnTypeWithGeneric // no references (23): // line 28 - WithGen // line 32 - t 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 61046bd23a6b1..18b58e4080c12 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 @@ -87,6 +87,10 @@ export const literalString = 'HI'; // missing comments (2): // line 32 - a // line 45 - foo +// param doc mismatches (3): +// line 19 - notAnArrowFn +// line 24 - aPropertyMisdirection +// line 29 - aPropertyInlineFn // no references (14): // line 18 - aPretendNamespaceObj // line 19 - notAnArrowFn 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 033d50ffe845b..6cbcd63764c24 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 @@ -109,6 +109,9 @@ export const iShouldBeInternalFn = () => 'hi'; // line 83 - a // line 83 - fnWithNonExportedRef // line 85 - NotAnArrowFnType +// param doc mismatches (2): +// line 83 - fnWithNonExportedRef +// line 85 - NotAnArrowFnType // no references (40): // line 13 - notAnArrowFn // line 24 - a 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 336f884675976..64d665cf9a966 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 @@ -45,6 +45,8 @@ export function plugin() { // line 30 - config // line 30 - foo // line 30 - new +// param doc mismatches (1): +// line 30 - new // any usage (1): // line 20 - imAnAny // no references (9): 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 bb1c09732b285..245793054f63b 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 @@ -186,6 +186,8 @@ export class PluginA implements PluginMock { // line 135 - fn // line 135 - foo // line 135 - param +// param doc mismatches (1): +// line 135 - fn // no references (23): // line 19 - SearchSpec // line 24 - username 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 52135cda0ec76..e396784906664 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 @@ -80,6 +80,10 @@ export type AReactElementFn = () => ReactElement; // line 61 - foo // line 62 - bar // line 65 - AReactElementFn +// param doc mismatches (3): +// line 30 - FnTypeWithGeneric +// line 54 - foo +// line 62 - bar // no references (21): // line 14 - StringOrUndefinedType // line 19 - TypeWithGeneric 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 797a7b2ddbb75..6b436988f1a3e 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 @@ -152,10 +152,12 @@ beforeAll(async () => { apiCount: pluginAStats.apiCount, missingExports: pluginAStats.missingExports, missingComments: pluginAStats.missingComments.length, + paramDocMismatches: pluginAStats.paramDocMismatches.length, isAnyType: pluginAStats.isAnyType.length, noReferences: pluginAStats.noReferences.length, }, missingComments: pluginAStats.missingComments.map(mapStat), + paramDocMismatches: pluginAStats.paramDocMismatches.map(mapStat), isAnyType: pluginAStats.isAnyType.map(mapStat), noReferences: pluginAStats.noReferences.map(mapStat), }; 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 d12986c1b172c..b482cd94fb6e0 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: 2026-01-28 +date: 2026-02-10 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 index db8258e3e2d4c..3ab7a37d4f29f 100644 --- a/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.stats.json +++ b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_a.stats.json @@ -3,6 +3,7 @@ "apiCount": 136, "missingExports": 2, "missingComments": 64, + "paramDocMismatches": 13, "isAnyType": 1, "noReferences": 135 }, @@ -520,6 +521,112 @@ "columnNumber": 3 } ], + "paramDocMismatches": [ + { + "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.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.fnWithNonExportedRef", + "label": "fnWithNonExportedRef", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Function", + "lineNumber": 83, + "columnNumber": 14 + }, + { + "id": "def-public.NotAnArrowFnType", + "label": "NotAnArrowFnType", + "path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/fns.ts", + "type": "Type", + "lineNumber": 85, + "columnNumber": 1 + }, + { + "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.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.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.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", + "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", + "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.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.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.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 + } + ], "isAnyType": [ { "id": "def-public.imAnAny", 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 42d7c930d0447..f94bd3e30c522 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: 2026-01-28 +date: 2026-02-10 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.mdx b/packages/kbn-docs-utils/src/integration_tests/snapshots/plugin_b.mdx index 1cc0bcff34edf..50c9e17514efc 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: 2026-01-28 +date: 2026-02-10 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginB'] --- import pluginBObj from './plugin_b.devdocs.json'; diff --git a/packages/kbn-docs-utils/src/stats.test.ts b/packages/kbn-docs-utils/src/stats.test.ts index b89aa2a493a7e..c684741cfe865 100644 --- a/packages/kbn-docs-utils/src/stats.test.ts +++ b/packages/kbn-docs-utils/src/stats.test.ts @@ -478,6 +478,272 @@ describe('collectApiStatsForPlugin', () => { }); }); + describe('param doc mismatches detection', () => { + it('flags functions where not all parameters have documentation', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'fn-partial-docs', + label: 'fnWithPartialDocs', + type: TypeKind.FunctionKind, + description: ['Function description'], + children: [ + createMockApiDeclaration({ + id: 'param-documented', + label: 'a', + description: ['Documented param'], + }), + createMockApiDeclaration({ + id: 'param-undocumented', + label: 'b', + description: undefined, + }), + ], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); + + expect(stats.paramDocMismatches).toHaveLength(1); + expect(stats.paramDocMismatches[0].id).toBe('fn-partial-docs'); + }); + + it('does not flag functions where all parameters have documentation', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'fn-all-docs', + label: 'fnWithAllDocs', + type: TypeKind.FunctionKind, + description: ['Function description'], + children: [ + createMockApiDeclaration({ + id: 'param-a', + label: 'a', + description: ['Param a description'], + }), + createMockApiDeclaration({ + id: 'param-b', + label: 'b', + description: ['Param b description'], + }), + ], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); + + expect(stats.paramDocMismatches).toHaveLength(0); + }); + + it('does not flag functions with no parameters', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'fn-no-params', + label: 'fnNoParams', + type: TypeKind.FunctionKind, + description: ['Function with no params'], + children: undefined, + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); + + expect(stats.paramDocMismatches).toHaveLength(0); + }); + + it('does not flag non-function types', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'interface-with-props', + label: 'InterfaceWithProps', + type: TypeKind.InterfaceKind, + description: ['Interface description'], + children: [ + createMockApiDeclaration({ + id: 'prop-undocumented', + label: 'prop', + description: undefined, + }), + ], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); + + expect(stats.paramDocMismatches).toHaveLength(0); + }); + + it('detects function-like type aliases via arrow signature', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'fn-type-alias', + label: 'FnTypeAlias', + type: TypeKind.TypeKind, + signature: ['(a: string, b: number) => void'], + description: ['Type alias for a function'], + children: [ + createMockApiDeclaration({ + id: 'param-a', + label: 'a', + description: ['Documented'], + }), + createMockApiDeclaration({ + id: 'param-b', + label: 'b', + description: undefined, + }), + ], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); + + expect(stats.paramDocMismatches).toHaveLength(1); + expect(stats.paramDocMismatches[0].id).toBe('fn-type-alias'); + }); + + it('flags functions with no params documented', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'fn-no-docs', + label: 'fnNoDocs', + type: TypeKind.FunctionKind, + description: ['Function description'], + children: [ + createMockApiDeclaration({ + id: 'param-a', + label: 'a', + description: undefined, + }), + createMockApiDeclaration({ + id: 'param-b', + label: 'b', + description: [], + }), + ], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); + + expect(stats.paramDocMismatches).toHaveLength(1); + expect(stats.paramDocMismatches[0].id).toBe('fn-no-docs'); + }); + + it('recursively checks nested functions for param doc mismatches', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'parent-interface', + label: 'ParentInterface', + type: TypeKind.InterfaceKind, + description: ['Interface'], + children: [ + createMockApiDeclaration({ + id: 'nested-fn', + label: 'nestedFn', + type: TypeKind.FunctionKind, + description: ['Nested function'], + children: [ + createMockApiDeclaration({ + id: 'nested-param', + label: 'param', + description: undefined, + }), + ], + }), + ], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); + + expect(stats.paramDocMismatches).toHaveLength(1); + expect(stats.paramDocMismatches[0].id).toBe('nested-fn'); + }); + + it('checks all scopes for param doc mismatches', () => { + const createFnWithUndocumentedParam = (id: string) => + createMockApiDeclaration({ + id, + type: TypeKind.FunctionKind, + description: ['Function'], + children: [ + createMockApiDeclaration({ + id: `${id}-param`, + label: 'param', + description: undefined, + }), + ], + }); + + const pluginApi = createMockPluginApi({ + client: [createFnWithUndocumentedParam('client-fn')], + server: [createFnWithUndocumentedParam('server-fn')], + common: [createFnWithUndocumentedParam('common-fn')], + }); + + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); + + expect(stats.paramDocMismatches).toHaveLength(3); + expect(stats.paramDocMismatches.map((d) => d.id)).toEqual([ + 'client-fn', + 'server-fn', + 'common-fn', + ]); + }); + + it('ignores node_modules paths', () => { + const pluginApi = createMockPluginApi({ + client: [ + createMockApiDeclaration({ + id: 'node-modules-fn', + type: TypeKind.FunctionKind, + description: ['Function'], + path: 'node_modules/some-package/index.ts', + children: [ + createMockApiDeclaration({ + id: 'node-modules-param', + label: 'param', + description: undefined, + }), + ], + }), + createMockApiDeclaration({ + id: 'regular-fn', + type: TypeKind.FunctionKind, + description: ['Function'], + path: 'src/plugin/file.ts', + children: [ + createMockApiDeclaration({ + id: 'regular-param', + label: 'param', + description: undefined, + }), + ], + }), + ], + }); + + const stats = collectApiStatsForPlugin(pluginApi, createEmptyIssues()); + + expect(stats.paramDocMismatches).toHaveLength(1); + expect(stats.paramDocMismatches[0].id).toBe('regular-fn'); + }); + }); + describe('deprecation tracking', () => { it('counts referenced deprecations', () => { const referencedDeprecations: ReferencedDeprecationsByPlugin = { diff --git a/packages/kbn-docs-utils/src/stats.ts b/packages/kbn-docs-utils/src/stats.ts index 4e78b605963fd..13b1731c51782 100644 --- a/packages/kbn-docs-utils/src/stats.ts +++ b/packages/kbn-docs-utils/src/stats.ts @@ -26,6 +26,7 @@ export function collectApiStatsForPlugin(doc: PluginApi, issues: IssuesByPlugin) missingComments: [], isAnyType: [], noReferences: [], + paramDocMismatches: [], deprecatedAPIsReferencedCount: 0, unreferencedDeprecatedApisCount: 0, adoptionTrackedAPIs: [], @@ -73,6 +74,8 @@ function collectStatsForApi(doc: ApiDeclaration, stats: ApiStats, pluginApi: Plu stats.missingComments.push(doc); } + trackParamDocMismatches(doc, stats); + if (doc.type === TypeKind.AnyKind) { stats.isAnyType.push(doc); } @@ -86,6 +89,47 @@ function collectStatsForApi(doc: ApiDeclaration, stats: ApiStats, pluginApi: Plu } } +/** + * Returns true if a declaration represents a function-like construct. + * + * This checks two conditions: + * 1. The declaration has `type: FunctionKind` - covers function declarations, method signatures, + * and function-typed properties in interfaces/classes. + * 2. The signature contains `=>` - covers type aliases that define function types. The API doc + * system normalizes all function signatures to arrow syntax, so this check is reliable. + */ +const isFunctionLike = (doc: ApiDeclaration): boolean => { + if (doc.type === TypeKind.FunctionKind) return true; + if (doc.signature) { + const sig = doc.signature.map((part) => (typeof part === 'string' ? part : part.text)).join(''); + return sig.includes('=>'); + } + return false; +}; + +/** + * Tracks functions where not all parameters have documentation. + * + * For function-like declarations, `children` represents the function's parameters. + * This is distinct from interface/class children which represent properties/methods. + * Each function-like member within an interface has its own declaration with its own + * children (parameters), so we don't conflate interface properties with function parameters. + */ +const trackParamDocMismatches = (doc: ApiDeclaration, stats: ApiStats): void => { + if (!isFunctionLike(doc)) { + return; + } + if (!doc.children || doc.children.length === 0) { + return; + } + const describedParams = doc.children.filter( + (param) => param.description && param.description.length > 0 + ).length; + if (describedParams !== doc.children.length) { + stats.paramDocMismatches.push(doc); + } +}; + function countApiForPlugin(doc: PluginApi) { return ( doc.client.reduce((sum, def) => { diff --git a/packages/kbn-docs-utils/src/types.ts b/packages/kbn-docs-utils/src/types.ts index 1378cce34be1e..0721feb54dc16 100644 --- a/packages/kbn-docs-utils/src/types.ts +++ b/packages/kbn-docs-utils/src/types.ts @@ -293,6 +293,7 @@ export interface ApiStats { missingComments: ApiDeclaration[]; isAnyType: ApiDeclaration[]; noReferences: ApiDeclaration[]; + paramDocMismatches: ApiDeclaration[]; apiCount: number; missingExports: number; deprecatedAPIsReferencedCount: number;