diff --git a/src/Clause.ts b/src/Clause.ts index bfe04b5b..58cde076 100644 --- a/src/Clause.ts +++ b/src/Clause.ts @@ -89,7 +89,10 @@ export default class Clause extends Builder { const type = this.node.getAttribute('type'); - const { name, formattedHeader, formattedParams } = parseStructuredHeaderH1(this.spec, header); + const { name, formattedHeader, formattedParams, formattedReturnType } = parseStructuredHeaderH1( + this.spec, + header + ); if (type === 'numeric method' && name != null && !name.includes('::')) { this.spec.warn({ type: 'contents', @@ -126,6 +129,7 @@ export default class Clause extends Builder { type, name ?? 'UNKNOWN', formattedParams ?? 'UNPARSEABLE ARGUMENTS', + formattedReturnType, _for, description ); diff --git a/src/header-parser.ts b/src/header-parser.ts index c7286db5..274386f2 100644 --- a/src/header-parser.ts +++ b/src/header-parser.ts @@ -4,7 +4,12 @@ import { offsetToLineAndColumn } from './utils'; export function parseStructuredHeaderH1( spec: Spec, header: Element -): { name: string | null; formattedHeader: string | null; formattedParams: string | null } { +): { + name: string | null; + formattedHeader: string | null; + formattedParams: string | null; + formattedReturnType: string | null; +} { // parsing is intentionally permissive; the linter can do stricter checks // TODO have the linter do checks @@ -26,7 +31,7 @@ export function parseStructuredHeaderH1( } const parsed = headerText.match( - /^(?\s*(?[^(\s]+)\s*)(?:\((?.*)\)\s*)?$/s + /^(?\s*(?[^(\s]+)\s*)(?:\((?[^)]*)\)(?:\s*:(?.*))?\s*)?$/s ); if (parsed == null) { spec.warn({ @@ -37,12 +42,32 @@ export function parseStructuredHeaderH1( nodeRelativeColumn: 1, nodeRelativeLine: 1, }); - return { name: null, formattedHeader: null, formattedParams: null }; + return { name: null, formattedHeader: null, formattedParams: null, formattedReturnType: null }; } type Param = { name: string; type: string | null; wrapper: string | null }; const name = parsed.groups!.name; let paramText = parsed.groups!.params ?? ''; + const returnType = parsed.groups!.returnType?.trim() ?? null; + + if (returnType === '') { + const { column, line } = offsetToLineAndColumn( + header.innerHTML, + beforeContents + + (prefix?.[0].length ?? 0) + + (headerText.length - headerText.match(/\s*$/)![0].length) - + 1 + ); + spec.warn({ + type: 'contents', + ruleId: 'header-format', + message: `if a return type is given, it must not be empty`, + node: header, + nodeRelativeColumn: column, + nodeRelativeLine: line, + }); + } + const params: Array = []; const optionalParams: Array = []; let formattedHeader = null; @@ -192,7 +217,7 @@ export function parseStructuredHeaderH1( formattedHeader = `<${wrapper}>${formattedHeader}`; } - return { name, formattedHeader, formattedParams }; + return { name, formattedHeader, formattedParams, formattedReturnType: returnType }; } export function parseStructuredHeaderDl( @@ -290,6 +315,7 @@ export function formatPreamble( type: string | null, name: string, formattedParams: string, + formattedReturnType: string | null, _for: Element | null, description: Element | null ): Array { @@ -300,20 +326,20 @@ export function formatPreamble( case 'numeric method': case 'abstract operation': { // TODO tests (for each type of parametered thing) which have HTML in the parameter type - para.innerHTML += `The abstract operation ${name} takes ${formattedParams}.`; + para.innerHTML += `The abstract operation ${name}`; break; } case 'host-defined abstract operation': { - para.innerHTML += `The host-defined abstract operation ${name} takes ${formattedParams}.`; + para.innerHTML += `The host-defined abstract operation ${name}`; break; } case 'implementation-defined abstract operation': { - para.innerHTML += `The implementation-defined abstract operation ${name} takes ${formattedParams}.`; + para.innerHTML += `The implementation-defined abstract operation ${name}`; break; } case 'sdo': case 'syntax-directed operation': { - para.innerHTML += `The syntax-directed operation ${name} takes ${formattedParams}.`; + para.innerHTML += `The syntax-directed operation ${name}`; break; } case 'internal method': @@ -330,7 +356,6 @@ export function formatPreamble( _for = spec.doc.createElement('div'); } para.append(`The ${name} ${type} of `, ..._for.childNodes); - para.innerHTML += ` takes ${formattedParams}.`; break; } default: { @@ -352,6 +377,11 @@ export function formatPreamble( } } } + para.innerHTML += ` takes ${formattedParams}`; + if (formattedReturnType != null) { + para.innerHTML += ` and returns ${formattedReturnType}`; + } + para.innerHTML += '.'; if (description != null) { const isJustElements = [...description.childNodes].every( n => n.nodeType === 1 || (n.nodeType === 3 && n.textContent?.trim() === '') diff --git a/test/baselines/generated-reference/structured-headers.html b/test/baselines/generated-reference/structured-headers.html index 19317aa6..6d958fa1 100644 --- a/test/baselines/generated-reference/structured-headers.html +++ b/test/baselines/generated-reference/structured-headers.html @@ -59,4 +59,10 @@

4.2 IsThat

  1. Return true.
+ + +

5 ExampleAO3 ( param )

+

The abstract operation ExampleAO3 takes argument param (an integer) and returns the return type. It performs the following steps when called:

+
  1. Algorithm steps go here.
+
\ No newline at end of file diff --git a/test/baselines/sources/structured-headers.html b/test/baselines/sources/structured-headers.html index dc438f0f..1a648eeb 100644 --- a/test/baselines/sources/structured-headers.html +++ b/test/baselines/sources/structured-headers.html @@ -89,3 +89,16 @@

+ + +

+ ExampleAO3 ( + param : an integer, + ): the return type +

+
+
+ + 1. Algorithm steps go here. + +
diff --git a/test/errors.js b/test/errors.js index 6d8f02b0..e12ed2e9 100644 --- a/test/errors.js +++ b/test/errors.js @@ -794,4 +794,22 @@ ${M} ); }); }); + + it('empty return types', async () => { + await assertError( + positioned` + +

Foo ( )${M}:

+
+
+
+ `, + { + ruleId: 'header-format', + nodeType: 'h1', + message: 'if a return type is given, it must not be empty', + }, + { lintSpec: true } + ); + }); });