Skip to content

chore(components): create eslint rule for stencil props initialization #5505

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

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4553c6f
chore(components): review props initialization
myrta2302 May 12, 2025
e61ec05
add changeset
myrta2302 May 12, 2025
15a340b
e2e test update
myrta2302 May 12, 2025
ba230a9
revert test changes
myrta2302 May 12, 2025
8329fa9
revert test file
myrta2302 May 12, 2025
d1d97b9
added partial changes to test file
myrta2302 May 12, 2025
09076be
fix e2e errors
myrta2302 May 12, 2025
127d5e6
Merge branch 'main' into chore(components)-review-prop-initialization
myrta2302 May 12, 2025
72244f5
revert checkoneof
myrta2302 May 12, 2025
72f298b
Merge branch 'chore(components)-review-prop-initialization' of https:…
myrta2302 May 12, 2025
8fda6ca
fix e2e error
myrta2302 May 12, 2025
317964b
Merge branch 'main' into chore(components)-review-prop-initialization
myrta2302 May 12, 2025
f729114
chore(eslint): add custom rule for strict props initialization
myrta2302 May 15, 2025
e407f10
add design-system-eslint to components and udpate rule
myrta2302 May 15, 2025
b180a9e
revert index and update config name
myrta2302 May 15, 2025
eecd6a1
update rule doc location
myrta2302 May 15, 2025
b8ed2be
update rule doc
myrta2302 May 15, 2025
3d1b55f
rename ruleset
myrta2302 May 15, 2025
553ae34
pnpm lock
myrta2302 May 16, 2025
625b8db
errors
myrta2302 May 16, 2025
898db25
add rule test
myrta2302 May 16, 2025
9b61676
fix lint error
myrta2302 May 16, 2025
c8a6faf
add build eslint package to lint.yaml
myrta2302 May 16, 2025
867f6a2
Merge branch 'main' into chore(components)-create-eslint-rule-for-ste…
myrta2302 May 28, 2025
00aa59f
revert files
myrta2302 May 28, 2025
f27c265
revert typescript version
myrta2302 Jun 2, 2025
71ebe6c
Merge branch 'main' into chore(components)-create-eslint-rule-for-ste…
myrta2302 Jun 2, 2025
a7a6772
revert package.jsons
myrta2302 Jun 2, 2025
707a5e7
update comment
myrta2302 Jun 2, 2025
1ae2efc
pnpm.lock
myrta2302 Jun 2, 2025
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
3 changes: 3 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
- name: Install dependencies of changed packages
run: pnpm install

- name: Build eslint package
run: pnpm --filter @swisspost/design-system-eslint build

# Test all changed packages and their dependents
# https://pnpm.io/filtering#--filter-since
- name: Lint packages
Expand Down
2 changes: 2 additions & 0 deletions packages/components/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { fixupConfigRules, fixupPluginRules } from '@eslint/compat';
import reactPlugin from 'eslint-plugin-react';
import stencilCommunityPlugin from '@stencil-community/eslint-plugin';
import pluginCypress from 'eslint-plugin-cypress/flat';
import { configs as dsEslintConfigs } from '@swisspost/design-system-eslint';

const compatStencilCommunityBaseRules = fixupConfigRules(stencilCommunityPlugin.configs.base)[0]
.overrides[0].rules;
Expand Down Expand Up @@ -54,6 +55,7 @@ export default [
},
},
...ts.configs.recommended,
dsEslintConfigs.stencilRecommended,
{
files: ['**/*.{js,mjs,cjs}'],
...ts.configs.disableTypeChecked,
Expand Down
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@oddbird/popover-polyfill": "0.5.2",
"@swisspost/design-system-icons": "workspace:10.0.0-next.38",
"@swisspost/design-system-styles": "workspace:10.0.0-next.38",
"@swisspost/design-system-eslint": "workspace:1.0.1-next.0",
"ally.js": "1.4.1",
"long-press-event": "2.5.0",
"nanoid": "5.1.5"
Expand Down
4 changes: 3 additions & 1 deletion packages/components/src/components/post-tag/post-tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ export class PostTag {
* <span className="banner banner-sm banner-info">If not set the icon will not show up.</span>
* To learn which icons are available, please visit our <a href="/?path=/docs/0dcfe3c0-bfc0-4107-b43b-7e9d825b805f--docs">icon library</a>.
*/
@Prop() readonly icon: string;

@Prop() readonly icon?: string;


constructor() {
this.setClasses = this.setClasses.bind(this);
Expand Down
29 changes: 29 additions & 0 deletions packages/eslint/docs/rules/strict-props-initialization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# `strict-props-initialization`

Reports any Stencil component `@Prop` that lacks an initial value and is neither marked as optional (?) or definitely assigned (!).',

- Type: problem

## Rule Options

This rule does not have any configuration options.

## Example

### ❌ Invalid Code

```ts
@Prop() myProp: string;
```

### ✅ Valid Code

```ts
@Prop() myProp!: string;
```

Or

```ts
@Prop() myProp?: string;
```
2 changes: 1 addition & 1 deletion packages/eslint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"description": "A set of custom ESLint rules to help keeping projects using the Swiss Post Design System up-to-date.",
"author": "Swiss Post <[email protected]>",
"license": "Apache-2.0",
"main": "dist/index.ts",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"repository": {
"type": "git",
Expand Down
28 changes: 28 additions & 0 deletions packages/eslint/src/configs/stencil/recommended.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { TSESLint } from '@typescript-eslint/utils';

// Define the recommended Stencil configuration
const stencilRecommendedConfig = (
stencilPlugin: TSESLint.FlatConfig.Plugin,
parser: TSESLint.FlatConfig.Parser,
): TSESLint.FlatConfig.Config => ({
name: '@swisspost/design-system-eslint/stencil/recommended',
files: ['**/*.{ts,tsx}'], // Apply to TypeScript and TSX files
languageOptions: {
parser, // Use the provided parser
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
},
plugins: {
'@swisspost/design-system-eslint': stencilPlugin,
},
rules: {
'@swisspost/design-system-eslint/stencil-strict-props-initialization': 'error',
},
});

export default stencilRecommendedConfig;
8 changes: 7 additions & 1 deletion packages/eslint/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { htmlParser } from './parsers/html';

import { htmlRules } from './rules/html';
import { tsRules } from './rules/ts';
import { dsLintingRules } from './rules';

import htmlAllConfig from './configs/html/all';
import htmlRecommendedConfig from './configs/html/recommended';
import tsAllConfig from './configs/ts/all';
import tsRecommendedConfig from './configs/ts/recommended';
import stencilRecommendedConfig from './configs/stencil/recommended';

const htmlPlugin: TSESLint.FlatConfig.Plugin = {
rules: htmlRules,
Expand All @@ -18,7 +20,10 @@ const htmlPlugin: TSESLint.FlatConfig.Plugin = {
};

const tsPlugin: TSESLint.FlatConfig.Plugin = {
rules: tsRules,
rules: {
...tsRules, // Include existing (currently empty) TS rules
...dsLintingRules, // Include Design System Sepcific
},
meta: {
name: '@swisspost/eslint-plugin-design-system',
},
Expand All @@ -29,6 +34,7 @@ const configs = {
htmlRecommended: htmlRecommendedConfig(htmlPlugin, htmlParser),
tsAll: tsAllConfig(tsPlugin, parser),
tsRecommended: tsRecommendedConfig(tsPlugin, parser),
stencilRecommended: stencilRecommendedConfig(tsPlugin, parser),
};

/* default and named exports allow people to use this package from both CJS and ESM. */
Expand Down
7 changes: 7 additions & 0 deletions packages/eslint/src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import stencilStrictPropsInitializationRule, {
name as stencilStrictPropsInitializationRuleName,
} from './stencil-strict-props-initialization';

export const dsLintingRules = {
[stencilStrictPropsInitializationRuleName]: stencilStrictPropsInitializationRule,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { createRule } from '../utils/create-rule';
import { AST_NODE_TYPES } from '@typescript-eslint/utils';

export const name = 'stencil-strict-props-initialization';

export default createRule({
name,
meta: {
docs: {
dir: 'ts',
description:
'Reports any Stencil component @Prop properties that lack an initial value and are neither marked as optional (?) nor definitely assigned (!).',
},
messages: {
propStrictInit:
"The '@Prop' property '{{propertyName}}' must have an initial value, or be explicitly marked as optional (?) or definitely assigned (!).",
},
type: 'suggestion',
schema: [],
},
defaultOptions: [],

create(context) {
return {
PropertyDefinition(node) {
if (node.decorators && node.decorators.length > 0) {
// Check if any decorator is @Prop()
const isProp = node.decorators.some(decorator => {
// Check for CallExpression like @Prop()
if (decorator.expression.type === AST_NODE_TYPES.CallExpression) {
const callee = decorator.expression.callee;
// Check if the callee is an Identifier named 'Prop'
if (callee.type === AST_NODE_TYPES.Identifier && callee.name === 'Prop') {
return true;
}
}
return false; // Ignore other decorator types or CallExpressions with different callees
});
if (isProp) {
// Check if the property is optional (?) or definitely assigned (!)
const isOptional = node.optional;
const isDefinitelyAssigned = node.definite;
// Also check if the property has no initial value set
if (!isOptional && !isDefinitelyAssigned && node.value === null) {
context.report({
node,
messageId: 'propStrictInit',
data: {
propertyName:
node.key.type === AST_NODE_TYPES.Identifier ? node.key.name : 'unknown',
},
});
}
}
}
},
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import rule, { name } from '../../src/rules/stencil-strict-props-initialization';
import { RuleTester } from '@typescript-eslint/rule-tester';
import { AST_NODE_TYPES } from '@typescript-eslint/utils'; // Import AST_NODE_TYPES

const ruleTester = new RuleTester();

ruleTester.run(name, rule, {
valid: [
//Code examples that should NOT trigger the rule
`
class MyComponent {
@Prop() myProp?:string;
}
`,
`
class MyComponent {
@Prop() myProp!:string;
}
`,
// Valid case for a property with an initial value
`
class MyComponent {
@Prop() myProp = 'initial value';
}
`,
],
invalid: [
{
// Code that SHOULD trigger the rule
code: `
class MyComponent {
@Prop() myProp: string; // Not optional, not definitely assigned, no initial value
}
`,
errors: [
{
messageId: 'propStrictInit',
type: AST_NODE_TYPES.PropertyDefinition, // The AST node type the rule checks (PropertyDefinition for class properties)
line: 3,
column: 11,
},
],
},
],
});
25 changes: 14 additions & 11 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.