Skip to content

Commit

Permalink
feat(config): add support for restrictionKeys
Browse files Browse the repository at this point in the history
  • Loading branch information
matthieu-crouzet committed Dec 17, 2024
1 parent ebd320a commit aadaf2e
Show file tree
Hide file tree
Showing 12 changed files with 352 additions and 3 deletions.
6 changes: 6 additions & 0 deletions apps/showcase/eslint.local.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ export default [
}
}
}
],
'@o3r/o3r-restriction-keys-tags': [
'error',
{
supportedKeys: ['restriction_1', 'restriction 2']
}
]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface ConfigurationPresConfig extends Configuration {
* @o3rWidgetParam allDestinationsDifferent true
* @o3rWidgetParam atLeastOneDestinationAvailable true
* @o3rWidgetParam destinationPattern "[A-Z][a-zA-Z-' ]+"
* @o3rRestrictionKey restriction_1
* @o3rRestrictionKey "restriction 2"
*/
destinations: DestinationConfiguration[];
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ export class ComponentConfigExtractor {
const res: ConfigProperty = {
description: configDocInfo?.description || '',
category: configDocInfo?.category,
restrictionKeys: configDocInfo?.restrictionKeys,
label: configDocInfo?.label || name.replace(/([A-Z])/g, ' $1'),
name,
type: 'unknown',
Expand Down
2 changes: 2 additions & 0 deletions packages/@o3r/components/src/core/component.output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export interface ConfigProperty {
widget?: ConfigPropertyWidget;
/** If true, the CMS user must specify a value for the property */
required?: boolean;
/** Restriction keys */
restrictionKeys?: string[];
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,13 @@
"items": {
"type": "string"
}
},
"restrictionKeys": {
"type": "array",
"description": "Restriction keys",
"items": {
"type": "string"
}
}
},
"allOf": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module.exports = {
'jsdoc/check-tag-names': [
'warn',
{
definedTags: ['note', 'title', 'o3rCategory', 'o3rCategories', 'o3rWidget', 'o3rWidgetParam', 'o3rRequired']
definedTags: ['note', 'title', 'o3rRestrictionKey', 'o3rCategory', 'o3rCategories', 'o3rWidget', 'o3rWidgetParam', 'o3rRequired']
}
],
'jsdoc/check-types': 'warn',
Expand Down
2 changes: 1 addition & 1 deletion packages/@o3r/eslint-config/src/rules/typescript/jsdoc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const config = [
'jsdoc/check-tag-names': [
'error',
{
definedTags: ['note', 'title', 'o3rCategory', 'o3rCategories', 'o3rWidget', 'o3rWidgetParam', 'o3rRequired']
definedTags: ['note', 'title', 'o3rRestrictionKey', 'o3rCategory', 'o3rCategories', 'o3rWidget', 'o3rWidgetParam', 'o3rRequired']
}
],
'jsdoc/no-defaults': 'off',
Expand Down
4 changes: 3 additions & 1 deletion packages/@o3r/eslint-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import matchingConfigurationName from './rules/typescript/matching-configuration
import noFolderImportForModule from './rules/typescript/no-folder-import-for-module/no-folder-import-for-module';
import noMultipleTypeConfigurationProperty from './rules/typescript/no-multiple-type-configuration-property/no-multiple-type-configuration-property';
import o3rCategoriesTags from './rules/typescript/o3r-categories-tags/o3r-categories-tags';
import o3rRestrictionKeyTags from './rules/typescript/o3r-restriction-key-tags/o3r-restriction-key-tags';
import o3rWidgetTags from './rules/typescript/o3r-widget-tags/o3r-widget-tags';
import yarnrcPackageExtensionHarmonize from './rules/yaml/yarnrc-package-extensions-harmonize/yarnrc-package-extensions-harmonize';

Expand All @@ -18,7 +19,8 @@ module.exports = {
'matching-configuration-name': matchingConfigurationName,
'yarnrc-package-extensions-harmonize': yarnrcPackageExtensionHarmonize,
'no-multiple-type-configuration-property': noMultipleTypeConfigurationProperty,
'o3r-categories-tags': o3rCategoriesTags
'o3r-categories-tags': o3rCategoriesTags,
'o3r-restriction-keys-tags': o3rRestrictionKeyTags
},
configs: {
'@o3r/no-folder-import-for-module': 'error',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import o3rRestrictionKeyRule, {
O3rRestrictionKeyTagsRuleOption,
} from './o3r-restriction-key-tags';
const {
RuleTester
} = require('@typescript-eslint/rule-tester');

const ruleTester = new RuleTester();

const code = `
export interface Config extends Configuration {
/**
* @o3rRestrictionKey valid
* @o3rRestrictionKey valid_with_underscore
* @o3rRestrictionKey 'valid with space (single quote)'
* @o3rRestrictionKey "valid with space (double quote)"
* @o3rRestrictionKey '"valid" with double quote inside'
* @o3rRestrictionKey "'valid' with single quote inside"
* @o3rRestrictionKey valid_with_number_1
*/
prop: string;
}
`;

const supportedKeys = [
'valid',
'valid_with_underscore',
'valid with space',
'valid with space (single quote)',
'valid with space (double quote)',
'"valid" with double quote inside',
"'valid' with single quote inside",
'valid_with_number_1'
];

const options = [{ supportedKeys }] as const satisfies Readonly<[O3rRestrictionKeyTagsRuleOption]>;

const unknownKeys = [
`unknown_restriction`,
`"invalid quote'`,
`'another invalid quote"`,
`'unknown with single quote'`,
`"unknown with double quote"`
];

const getCodeFor = (key: string) => `
export interface Config extends Configuration {
/**
* @o3rRestrictionKey ${key}
*/
prop: string;
}
`;

const getSuggestionFor = (actualKey: string) => supportedKeys.map((supportedKey) => ({
messageId: 'suggestUseSupportedKey',
data: {
actualKey,
supportedKey
},
output: getCodeFor(/[^\w]/.test(supportedKey) ? `"${supportedKey}"` : supportedKey)
}));

ruleTester.run('o3r-restriction-keys-tags', o3rRestrictionKeyRule, {
valid: [
{
code,
options
},
{
code: `
export interface Config extends Configuration {
/**
* Property without restriction
*/
prop: string;
}`,
options
}
],
invalid: [
{
code: getCodeFor('"at least one key provided"'),
options: [{}],
errors: [
{ messageId: 'noRestrictionKeyProvided' }
]
},
{
code: code.replace(' extends Configuration', ''),
options,
errors: [
{
messageId: 'notInConfigurationInterface'
}
]
},
{
code: getCodeFor('valid with space'),
options,
output: getCodeFor(`"valid with space"`),
errors: [
{
messageId: 'notWrapWithQuotes',
data: {
actualKey: 'valid with space'
},
suggestions: [{
messageId: 'suggestWrapWithQuotes',
data: {
actualKey: 'valid with space'
},
output: getCodeFor(`"valid with space"`)
}]
}
]
},
...unknownKeys.map((key) => ({
code: getCodeFor(key),
options,
errors: [{
messageId: 'notSupportedKey',
data: {
actualKey: key,
supportedKeys: supportedKeys.join(', ')
},
suggestions: getSuggestionFor(key)
}]
}))
]
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import {
TSESLint,
type TSESTree,
} from '@typescript-eslint/utils';
import {
createCommentString,
createRule,
defaultSupportedInterfaceNames,
getNodeComment,
isExtendingConfiguration,
} from '../../utils';

export interface O3rRestrictionKeyTagsRuleOption {
supportedInterfaceNames?: string[];
supportedKeys?: string[];
}

type O3rWidgetRuleErrorId =
| 'notSupportedKey'
| 'notWrapWithQuotes'
| 'suggestWrapWithQuotes'
| 'suggestUseSupportedKey'
| 'noRestrictionKeyProvided'
| 'notInConfigurationInterface';

const defaultOptions: [Required<O3rRestrictionKeyTagsRuleOption>] = [{
supportedInterfaceNames: defaultSupportedInterfaceNames,
supportedKeys: []
}];

export default createRule<[Readonly<O3rRestrictionKeyTagsRuleOption>, ...any], O3rWidgetRuleErrorId>({
name: 'o3r-restriction-keys-tags',
meta: {
hasSuggestions: true,
fixable: 'code',
type: 'problem',
docs: {
description: 'Ensures that @o3rRestrictionKey is used with a correct value'
},
schema: [
{
type: 'object',
properties: {
supportedInterfaceNames: {
type: 'array',
items: {
type: 'string'
}
},
supportedKeys: {
type: 'array',
items: {
type: 'string'
},
minItems: 1
}
}
}
],
messages: {
notSupportedKey: '{{ actualKey }} is not supported. Supported restriction keys: {{ supportedKeys }}.',
suggestUseSupportedKey: '{{ actualKey }} is not supported. Replace with {{ supportedKey }}.',
notWrapWithQuotes: '`{{ actualKey }}` is not wrapped with quotes.',
suggestWrapWithQuotes: 'Wrap `{{ actualKey }}` with quotes.',
notInConfigurationInterface: '@o3rRestrictionKey can only be used in a `Configuration` interface.',
noRestrictionKeyProvided: 'You must have at least one restriction key.'
}
},
defaultOptions,
create: (context, [options]: Readonly<[O3rRestrictionKeyTagsRuleOption, ...any]>) => {
const rule = (node: TSESTree.TSPropertySignature) => {
const { sourceCode } = context;
const comment = getNodeComment(node, sourceCode);

if (!comment || comment.value.length === 0) {
return;
}

const { loc, value: docText } = comment;
const supportedKeys = options.supportedKeys || defaultOptions[0].supportedKeys;
const supportedKeysSet = new Set(supportedKeys);

if (supportedKeys.length === 0) {
return context.report({
messageId: 'noRestrictionKeyProvided',
node,
loc
});
}

const actualKeys = Array.from(docText.matchAll(/@o3rRestrictionKey\s+(.*)/g)).map((match) => match[1]);

if (actualKeys.length === 0) {
return;
}

const interfaceDeclNode = node.parent?.parent;
if (!isExtendingConfiguration(interfaceDeclNode, options.supportedInterfaceNames)) {
return context.report({
messageId: 'notInConfigurationInterface',
node,
loc
});
}

actualKeys.forEach((actualKey) => {
if (!supportedKeysSet.has(actualKey)) {
if (
/((["']).*?\2)/.test(actualKey)
&& supportedKeysSet.has(actualKey.replace(/(^["']|["']$)/g, ''))
) {
return;
}
const fix: (key: string) => TSESLint.ReportFixFunction = (key) => (fixer) => {
return fixer.replaceTextRange(comment.range, createCommentString(comment.value.replace(actualKey, /[^\w]/.test(key) ? `"${key}"` : key)));
};
return context.report({
messageId: 'notSupportedKey',
node,
loc,
data: {
actualKey,
supportedKeys: supportedKeys.join(', ')
},
suggest: supportedKeys.map((supportedKey) => ({
messageId: 'suggestUseSupportedKey',
data: {
actualKey,
supportedKey
},
fix: fix(supportedKey)
}))
});
}
if (/[^\w]/.test(actualKey)) {
const fix: TSESLint.ReportFixFunction = (fixer) => {
return fixer.replaceTextRange(comment.range, createCommentString(comment.value.replace(actualKey, `"${actualKey}"`)));
};
return context.report({
messageId: 'notWrapWithQuotes',
data: {
actualKey
},
node,
loc,
fix,
suggest: [{
messageId: 'suggestWrapWithQuotes',
data: {
actualKey
},
fix
}]
});
}
});
};

return {
TSPropertySignature: rule
};
}
});
Loading

0 comments on commit aadaf2e

Please sign in to comment.