Leon4gr45's picture
Upload folder using huggingface_hub
048b1e8 verified
Raw
History Blame Contribute Delete
5.57 kB
import {
createRule,
defineCodeFix,
getDoc,
getSourceLocation,
paramMessage,
} from '@typespec/compiler'
import * as prettier from 'prettier'
import {
detectNewline,
extractMarkdownFromDocComment,
getIndentBefore,
wrapMarkdownAsDocComment,
} from './utils.js'
export const docDecoratorRule = createRule({
name: 'doc-decorator',
severity: 'warning',
description: 'Ensure documentation.',
messages: {
default: paramMessage`Missing documentation for ${'name'} ${'type'}`,
},
create: (context) => ({
model: (target) => {
if (target.name && !getDoc(context.program, target)) {
context.reportDiagnostic({
target,
format: {
name: target.name,
},
})
}
if (target.name.endsWith('Response')) {
return
}
for (const [name, property] of target.properties) {
if (
target.name &&
name &&
!['_', 'contentType'].includes(name) &&
!getDoc(context.program, property)
) {
context.reportDiagnostic({
target: property,
format: {
name: `${target.name}.${name}`,
},
})
}
}
},
enum: (target) => {
if (target.name && !getDoc(context.program, target)) {
context.reportDiagnostic({
target,
format: {
name: target.name,
},
})
}
},
union: (target) => {
if (target.name && !getDoc(context.program, target)) {
context.reportDiagnostic({
target,
format: {
name: target.name,
},
})
}
},
}),
})
/**
* Format a doc-comment Markdown body through Prettier.
* Returns the formatted Markdown body (no `/** *\/` framing).
* @param {string} markdown
* @param {{ printWidth?: number, proseWrap?: 'always' | 'never' | 'preserve' }} [options]
*/
async function formatDocMarkdown(markdown, options = {}) {
if (markdown.trim() === '') return ''
return await prettier.format(markdown, {
parser: 'markdown',
printWidth: options.printWidth ?? 80,
proseWrap: options.proseWrap ?? 'always',
})
}
/**
* Build a code fix that replaces a doc comment with a precomputed string.
* The Prettier work happens before this is constructed; the fix callback is
* sync and just emits the replacement.
*
* @param {import('@typespec/compiler').SourceLocation} location
* The full `/** ... *\/` source range.
* @param {string} newText The replacement text, including `/**` and `*\/`.
*/
function createFormatDocCommentCodeFix(location, newText) {
return defineCodeFix({
id: 'format-doc-comment',
label: 'Format doc comment',
fix(context) {
return context.replaceText(location, newText)
},
})
}
/**
* Collect every `DocNode` reachable from the program by walking semantic
* targets that can carry doc comments. We use the existing semantic listener
* surface (model/property/enum/etc.) rather than a private AST walker.
*/
function collectDocNodes(target, sink) {
const node = target.node
if (!node || !node.docs || node.docs.length === 0) return
for (const doc of node.docs) sink.push(doc)
}
export const docFormatRule = createRule({
name: 'doc-format',
severity: 'warning',
description:
'Format doc comment bodies as Markdown using Prettier (proseWrap=always).',
messages: {
default:
'Doc comment is not formatted. Apply the suggested fix to reformat as Markdown.',
},
// Async because Prettier 3.x's `format` is async.
async: true,
create: (context) => {
/** @type {import('@typespec/compiler').DocNode[]} */
const docNodes = []
const collect = (target) => collectDocNodes(target, docNodes)
return {
model: collect,
modelProperty: collect,
enum: collect,
enumMember: collect,
union: collect,
unionVariant: collect,
operation: collect,
interface: collect,
scalar: collect,
namespace: collect,
async exit() {
// Deduplicate: a doc may be visited via multiple semantic kinds.
const seen = new Set()
const work = []
for (const doc of docNodes) {
if (seen.has(doc)) continue
seen.add(doc)
work.push(processDoc(doc, context))
}
await Promise.all(work)
},
}
},
})
/**
* Compute a formatted replacement for a single DocNode and, if it differs
* from the source, report a diagnostic with an attached code fix.
* @param {import('@typespec/compiler').DocNode} doc
* @param {import('@typespec/compiler').LinterRuleContext<any>} context
*/
async function processDoc(doc, context) {
const location = getSourceLocation(doc)
const source = location.file.text
const raw = source.slice(location.pos, location.end)
// Defensive: only format actual `/** ... */` blocks.
if (!raw.startsWith('/**') || !raw.endsWith('*/')) return
const indent = getIndentBefore(source, location.pos)
const newline = detectNewline(source)
let markdown
try {
markdown = extractMarkdownFromDocComment(raw)
} catch {
return
}
let formatted
try {
formatted = await formatDocMarkdown(markdown)
} catch {
// If Prettier can't parse the body, leave it alone.
return
}
const replacement = wrapMarkdownAsDocComment(formatted, indent, newline)
if (replacement === raw) return
context.reportDiagnostic({
target: doc,
codefixes: [createFormatDocCommentCodeFix(location, replacement)],
})
}