Skip to content

Commit de2b4fb

Browse files
authored
feat(linting): add require-property-tsdoc rule (#5189)
* feat(linting): add require-property-tsdoc rule Adds a new fusion-lint rule requiring TSDoc comments on class field declarations, including Lit @Property()/@State() decorated fields. Private (accessibility modifier or #name) and static fields are exempt. Supports a classScope option ('all' | 'exported') to limit enforcement to exported classes. Included in the recommended preset. * docs: add missing TSDoc on class fields flagged by require-property-tsdoc Full-repo fusion-lint sweep after adding the new rule surfaced 13 pre-existing class fields without TSDoc. Documented each. * fix(linting): support decorated get/set accessors in require-property-tsdoc A decorated accessor's decorator is a sibling of the method_definition in class_body, not nested inside it like a field's decorator is, so the existing anchor logic and the public_field_definition-only node check both missed it. Decorated get/set pairs (e.g. Lit's @Property()-decorated getters) are now checked the same as decorated fields; undecorated accessors remain out of scope.
1 parent f35a34e commit de2b4fb

18 files changed

Lines changed: 429 additions & 1 deletion

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@equinor/fusion-framework-cookbook-app-react-router": patch
3+
"@equinor/fusion-framework-module-app": patch
4+
"@equinor/fusion-framework-module-analytics": patch
5+
"@equinor/fusion-framework-module-azure-identity": patch
6+
"@equinor/fusion-framework-module-bookmark": patch
7+
"@equinor/fusion-framework-module-feature-flag": patch
8+
"@equinor/fusion-framework-module-http": patch
9+
"@equinor/fusion-framework-module-services": patch
10+
"@equinor/fusion-query": patch
11+
---
12+
13+
Added missing TSDoc comments on class fields flagged by the new `require-property-tsdoc` fusion-lint rule.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@equinor/fusion-framework-lint-rules": minor
3+
"@equinor/fusion-framework-lint-config": minor
4+
---
5+
6+
Add `require-property-tsdoc` rule: requires class field (property) declarations — including Lit's `@property()` / `@state()` decorated fields — to have a preceding TSDoc block comment. `private`/`#name` and `static` fields are exempt. Included in the `recommended` rule preset.

cookbooks/app-react-router/src/api/Api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ interface IHttpProvider {
2121
* Uses React Query for caching and request deduplication
2222
*/
2323
export class Api {
24+
/** Product-related API client. */
2425
readonly product: ProductApi;
26+
/** People-related API client. */
2527
readonly people: PeopleApi;
28+
/** User-related API client. */
2629
readonly user: UserApi;
2730

2831
/**

packages/linting/config/src/recommended-rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
singleExportPerFile,
1616
requireComponentTsDoc,
1717
requireHookTsDoc,
18+
requirePropertyTsDoc,
1819
filenameConvention,
1920
} from '@equinor/fusion-framework-lint-rules';
2021

@@ -41,5 +42,6 @@ export const recommendedRules: Rule[] = [
4142
singleExportPerFile(),
4243
requireComponentTsDoc(),
4344
requireHookTsDoc(),
45+
requirePropertyTsDoc(),
4446
filenameConvention(),
4547
];
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { requirePropertyTsDoc } from '../require-property-tsdoc/index.js';
3+
import type { Diagnostic, Rule } from '@equinor/fusion-framework-lint-core';
4+
5+
function lint(
6+
source: string,
7+
file = 'fixture.ts',
8+
rule: Rule = requirePropertyTsDoc(),
9+
): Diagnostic[] {
10+
// mirror the engine: skip `check` entirely when `match` opts the file out
11+
if (rule.match && !rule.match(file)) return [];
12+
return rule.check(source, { filePath: file });
13+
}
14+
15+
describe('require-property-tsdoc — passing', () => {
16+
it('passes: documented public field', () => {
17+
const source = `
18+
class MyButton {
19+
/** The visual color variant to render. */
20+
color = 'primary';
21+
}
22+
`;
23+
expect(lint(source)).toHaveLength(0);
24+
});
25+
26+
it('passes: documented decorated field', () => {
27+
const source = `
28+
class MyButton {
29+
/** The visual color variant to render. */
30+
@property({ type: String })
31+
color = 'primary';
32+
}
33+
`;
34+
expect(lint(source)).toHaveLength(0);
35+
});
36+
37+
it('passes: private modifier field without TSDoc', () => {
38+
const source = `
39+
class MyButton {
40+
private internalState = 1;
41+
}
42+
`;
43+
expect(lint(source)).toHaveLength(0);
44+
});
45+
46+
it('passes: #private field without TSDoc', () => {
47+
const source = `
48+
class MyButton {
49+
#internalState = 1;
50+
}
51+
`;
52+
expect(lint(source)).toHaveLength(0);
53+
});
54+
55+
it('passes: static field without TSDoc', () => {
56+
const source = `
57+
class MyButton {
58+
static styles = [];
59+
}
60+
`;
61+
expect(lint(source)).toHaveLength(0);
62+
});
63+
64+
it('passes: field in non-exported class when classScope is "exported"', () => {
65+
const rule = requirePropertyTsDoc({ classScope: 'exported' });
66+
const source = `
67+
class MyButton {
68+
color = 'primary';
69+
}
70+
`;
71+
expect(lint(source, 'fixture.ts', rule)).toHaveLength(0);
72+
});
73+
74+
it('passes: documented decorated getter accessor', () => {
75+
const source = `
76+
class MyButton {
77+
/**
78+
* @deprecated use \`variant="contained"\` instead.
79+
* @returns true when \`variant\` is \`contained\`
80+
*/
81+
@property({ type: Boolean, reflect: true })
82+
get raised(): boolean {
83+
return this.variant === 'contained';
84+
}
85+
}
86+
`;
87+
expect(lint(source)).toHaveLength(0);
88+
});
89+
90+
it('passes: undecorated getter accessor without TSDoc', () => {
91+
const source = `
92+
class MyButton {
93+
get raised(): boolean {
94+
return this.variant === 'contained';
95+
}
96+
}
97+
`;
98+
expect(lint(source)).toHaveLength(0);
99+
});
100+
});
101+
102+
describe('require-property-tsdoc — failing', () => {
103+
it('fails: undocumented public field', () => {
104+
const source = `
105+
class MyButton {
106+
color = 'primary';
107+
}
108+
`;
109+
const diagnostics = lint(source);
110+
expect(diagnostics).toHaveLength(1);
111+
expect(diagnostics[0].message).toContain("'color'");
112+
});
113+
114+
it('fails: undocumented decorated field', () => {
115+
const source = `
116+
class MyButton {
117+
@property({ type: String })
118+
color = 'primary';
119+
}
120+
`;
121+
const diagnostics = lint(source);
122+
expect(diagnostics).toHaveLength(1);
123+
expect(diagnostics[0].message).toContain("'color'");
124+
});
125+
126+
it('fails: undocumented protected field', () => {
127+
const source = `
128+
class MyButton {
129+
protected color = 'primary';
130+
}
131+
`;
132+
expect(lint(source)).toHaveLength(1);
133+
});
134+
135+
it('fails: undocumented field in exported class when classScope is "exported"', () => {
136+
const rule = requirePropertyTsDoc({ classScope: 'exported' });
137+
const source = `
138+
export class MyButton {
139+
color = 'primary';
140+
}
141+
`;
142+
expect(lint(source, 'fixture.ts', rule)).toHaveLength(1);
143+
});
144+
145+
it('fails: non-TSDoc comment does not satisfy the rule', () => {
146+
const source = `
147+
class MyButton {
148+
// just a regular comment
149+
color = 'primary';
150+
}
151+
`;
152+
expect(lint(source)).toHaveLength(1);
153+
});
154+
155+
it('fails: undocumented decorated getter accessor', () => {
156+
const source = `
157+
class MyButton {
158+
@property({ type: Boolean, reflect: true })
159+
get raised(): boolean {
160+
return this.variant === 'contained';
161+
}
162+
}
163+
`;
164+
const diagnostics = lint(source);
165+
expect(diagnostics).toHaveLength(1);
166+
expect(diagnostics[0].message).toContain("'raised'");
167+
});
168+
});

packages/linting/rules/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ export { noSeparateExport } from './no-separate-export/index.js';
1616
export { singleExportPerFile } from './single-export-per-file/index.js';
1717
export { requireComponentTsDoc } from './require-component-tsdoc/index.js';
1818
export { requireHookTsDoc } from './require-hook-tsdoc/index.js';
19+
export { requirePropertyTsDoc } from './require-property-tsdoc/index.js';
20+
export type { RequirePropertyTsDocOptions } from './require-property-tsdoc/index.js';
1921
export { filenameConvention } from './filename-convention/index.js';

0 commit comments

Comments
 (0)