Add rules

This commit is contained in:
eledobleefe
2025-11-27 01:14:05 +01:00
parent 80c7d17543
commit 9acafa2b50
3 changed files with 179 additions and 0 deletions
+21
View File
@@ -181,3 +181,24 @@ When a violation is detected, the rule reports:
```
Import '../status-history/utils' reaches outside the 'histogram' plugin directory. Plugins should only import from external dependencies or relative paths within their own directory.
```
### `tracking-event-creation`
Checks that the process to create a tracking event is followed in the right way.
#### `eventFactoryLiterals`
Check if the values passed to `createEventFactory` are literals.
```tsx
// Bad ❌
const repo = 'grafana';
const createUnifiedHistoryEvent = createEventFactory(repo, 'unified_history');
// Bad ❌
const history = 'history';
const createUnifiedHistoryEvent = createEventFactory('grafana', `unified_${history}`);
// Good ✅
const createUnifiedHistoryEvent = createEventFactory('grafana', 'unified_history');
```
+2
View File
@@ -5,6 +5,7 @@ const themeTokenUsage = require('./rules/theme-token-usage.cjs');
const noRestrictedImgSrcs = require('./rules/no-restricted-img-srcs.cjs');
const consistentStoryTitles = require('./rules/consistent-story-titles.cjs');
const noPluginExternalImportPaths = require('./rules/no-plugin-external-import-paths.cjs');
const trackingEventCreation = require('./rules/tracking-event-creation.cjs');
module.exports = {
rules: {
@@ -15,5 +16,6 @@ module.exports = {
'no-restricted-img-srcs': noRestrictedImgSrcs,
'consistent-story-titles': consistentStoryTitles,
'no-plugin-external-import-paths': noPluginExternalImportPaths,
'tracking-event-creation': trackingEventCreation,
},
};
@@ -0,0 +1,156 @@
// @ts-check
const { ESLintUtils, AST_NODE_TYPES } = require('@typescript-eslint/utils');
const createRule = ESLintUtils.RuleCreator(
(name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}`
);
const trackingEventCreation = createRule({
create(context) {
// Track what name createEventFactory is imported as
let createEventFactoryName = 'createEventFactory';
// Track if createEventFactory is imported
let isCreateEventFactoryImported = false;
// Track variables that store createEventFactory calls
const eventFactoryVariables = new Set();
return {
ImportSpecifier(node) {
if (node.imported.type === AST_NODE_TYPES.Identifier && node.imported.name === 'createEventFactory') {
// Remember what name it was imported as (handles aliased imports)
createEventFactoryName = node.local.name;
isCreateEventFactoryImported = true;
}
},
VariableDeclarator(node) {
if (!isCreateEventFactoryImported) {
return;
}
// Track variables initialized with createEventFactory calls
if (
node.init?.type === AST_NODE_TYPES.CallExpression &&
node.init.callee.type === AST_NODE_TYPES.Identifier &&
node.init.callee.name === createEventFactoryName
) {
const variableName = node.id.type === AST_NODE_TYPES.Identifier && node.id.name;
if (variableName) {
eventFactoryVariables.add(variableName);
}
// Check if arguments are literals
const args = node.init.arguments;
const argsAreNotLiterals = args.some((arg) => arg.type !== AST_NODE_TYPES.Literal);
if (argsAreNotLiterals) {
return context.report({
node: node.init,
messageId: 'eventFactoryLiterals',
});
}
}
},
ExportNamedDeclaration(node) {
if (!isCreateEventFactoryImported) {
return;
}
if (
node.declaration?.type === AST_NODE_TYPES.VariableDeclaration &&
node.declaration.declarations[0].init?.type === AST_NODE_TYPES.CallExpression
) {
const callee = node.declaration.declarations[0].init.callee;
if (callee.type === AST_NODE_TYPES.Identifier && eventFactoryVariables.has(callee.name)) {
// Check for comments
// Check for comments
const comments = context.sourceCode.getCommentsBefore(node);
if (!comments || comments.length === 0) {
return context.report({
node,
messageId: 'missingFunctionComment',
});
}
const jsDocComment = comments.find((comment) => comment.value.slice(0, 1) === '*');
if (!jsDocComment) {
return context.report({
node,
messageId: 'missingJsDocComment',
});
}
}
}
},
TSInterfaceDeclaration(node) {
if (!isCreateEventFactoryImported) {
return;
}
// Check if interface extends TrackingEvent
let extendsTrackingEvent = false;
if (node.extends && node.extends.length > 0) {
const interfaceExtends = node.extends;
extendsTrackingEvent = interfaceExtends.some((extend) => {
return extend.expression.type === AST_NODE_TYPES.Identifier && extend.expression.name === 'TrackingEventProps';
});
}
if (!node.extends || !extendsTrackingEvent) {
return context.report({
node,
messageId: 'interfaceMustExtend',
});
}
//Check if the interface properties has comments
if(node.body.type === AST_NODE_TYPES.TSInterfaceBody){
const properties = node.body.body;
properties.forEach(property => {
const comments = context.sourceCode.getCommentsBefore(property);
if (!comments || comments.length === 0) {
return context.report({
node: property,
messageId: 'missingPropertyComment',
});
}
const jsDocComment = comments.find((comment) => comment.value.slice(0, 1) === '*');
if (!jsDocComment) {
return context.report({
node: property,
messageId: 'missingJsDocComment',
});
}
});
}
},
TSTypeAliasDeclaration(node) {
if (!isCreateEventFactoryImported) {
return;
}
// Check if types has comments
const comments = context.sourceCode.getCommentsBefore(node);
if (!comments || comments.length === 0) {
return context.report({
node,
messageId: 'missingPropertyComment',
});
}
}
};
},
name: 'tracking-event-creation',
meta: {
type: 'problem',
docs: {
description: 'Check that the tracking event is created in the right way',
},
messages: {
eventFactoryLiterals: 'Params passed to `createEventFactory` must be literals',
missingFunctionComment: 'Event function needs to have a description of its purpose',
missingPropertyComment: 'Event property needs to have a description of its purpose',
interfaceMustExtend: 'Interface must extend `TrackingEvent`',
missingJsDocComment: 'Comment needs to be a jsDoc comment (begin comment with `*`)',
},
schema: [],
},
defaultOptions: [],
});
module.exports = trackingEventCreation;