Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add enforce-slot-jsdoc rule #105

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/enforce-slot-jsdoc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# enforce-slot-jsdoc

This rule ensures proper slot documentation by verifying:

* Every slot defined in the component has a corresponding @slot JSDoc tag.
* Every @slot JSDoc tag matches a slot in the component.

**Note**: Only slots with static names can be detected for mismatches.

## Config

No config is needed

## Usage

```json
{ "@stencil-community/enforce-slot-jsdoc": "error" }
```
64 changes: 64 additions & 0 deletions src/rules/enforce-slot-jsdoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Rule } from 'eslint';
import { stencilComponentContext } from '../utils';

const rule: Rule.RuleModule = {
meta: {
docs: {
description: 'Ensures slots are documented with JSDoc.',
category: 'Possible Errors',
recommended: true,
},
schema: [],
type: 'problem',
},

create(context): Rule.RuleListener {
const stencil = stencilComponentContext();
const { parserServices } = context;
const implementedSlots = new Set<string>();

return {
...stencil.rules,
'ClassDeclaration:exit': (node: any) => {
if (stencil.isComponent()) {
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const jsDoc = originalNode.jsDoc;
const documentedSlots: Set<string> = new Set(jsDoc[0].tags
.filter((tag: any) => tag.tagName.escapedText === "slot")
.map((tag: any) => tag.comment.split("-")[0].trim() || "<default>")
);

const missingDocSlots = Array.from(implementedSlots).filter(slot => !documentedSlots.has(slot));
const nonImplementedSlots = Array.from(documentedSlots).filter(slot => !implementedSlots.has(slot));

missingDocSlots.forEach(slot => {
context.report({
node,
message: slot === "<default>" ? "The default @slot must be documented." : `The @slot '${slot}' must be documented.`,
});
});

nonImplementedSlots.forEach(slot => {
context.report({
node,
message: slot === "<default>" ? "The default @slot is not implemented." : `The @slot '${slot}' is not implemented.`,
});
});
}

implementedSlots.clear();
stencil.rules["ClassDeclaration:exit"](node);
},

JSXElement(node: any): void {
if (node.openingElement.name.name !== "slot") return;

const nameAttribute = node.openingElement.attributes.find((attribute: any) => attribute.name.name === "name");
const slotName = nameAttribute && nameAttribute.value ? nameAttribute.value.value : "<default>";
implementedSlots.add(slotName);
},
};
},
};

export default rule;
17 changes: 17 additions & 0 deletions tests/lib/rules/enforce-slot-jsdoc/enforce-slot-jsdoc.good.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @slot - The default slot
* @slot header - The header slot
* @slot footer - The footer slot
*/
@Component({ tag: 'sample-tag' })
export class TheSampleTag {
render() {
return (
<div>
<slot>hello</slot>
<slot name="header"></slot>
<slot name="footer"></slot>
</div>
);
}
}
29 changes: 29 additions & 0 deletions tests/lib/rules/enforce-slot-jsdoc/enforce-slot-jsdoc.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import rule from '../../../../src/rules/enforce-slot-jsdoc';
import { ruleTester } from '../rule-tester';
import * as path from 'path';
import * as fs from 'fs';

describe('stencil rules', () => {
const files = {
good: path.resolve(__dirname, 'enforce-slot-jsdoc.good.tsx'),
wrong: path.resolve(__dirname, 'enforce-slot-jsdoc.wrong.tsx')
};
const validCode = fs.readFileSync(files.good, 'utf8');

ruleTester.run('enforce-slot-jsdoc', rule, {
valid: [
{
code: validCode,
filename: files.good
}
],

invalid: [
{
code: fs.readFileSync(files.wrong, 'utf8'),
filename: files.wrong,
errors: 3,
}
]
});
});
14 changes: 14 additions & 0 deletions tests/lib/rules/enforce-slot-jsdoc/enforce-slot-jsdoc.wrong.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @slot - The default slot (not implemented)
* @slot header - The header slot (not implemented)
*/
@Component({ tag: 'sample-tag' })
export class TheSampleTag {
render() {
return (
<div>
<slot name="footer">not documented</slot>
</div>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('stencil rules', () => {
{
code: fs.readFileSync(files.wrong, 'utf8'),
filename: files.wrong,
errors: 2
errors: 3
}
]
});
Expand Down
Loading