@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
133 lines (123 loc) • 4.88 kB
JavaScript
import { isNodeOfType } from 'eslint-codemod-utils';
import { getSourceCode } from '@atlaskit/eslint-utils/context-compat';
import { createLintRule } from '../utils/create-lint-rule';
export var name = 'no-unsafe-inline-snapshot';
var MAX_LINES = 100;
/**
* Checks if a snapshot contains internal implementation details
*/
function containsInternalDetails(snapshotContent) {
var issues = [];
// Check for className attributes (unless they equal "REDACTED")
// Handles: className="value", className='value', and whitespace variations
var classNameRegex = /className\s*=\s*(["'])((?:(?!\1)[^\\]|\\.)*)\1/gi;
var match;
while ((match = classNameRegex.exec(snapshotContent)) !== null) {
var classNameValue = match[2];
if (classNameValue && classNameValue !== 'REDACTED') {
issues.push("className=\"".concat(classNameValue, "\""));
}
}
// Check for style attributes (unless they equal "REDACTED")
// Handles: style="value", style='value', and whitespace variations
// Style values can contain colons, semicolons, etc., so we need to capture the full quoted value
var styleRegex = /style\s*=\s*(["'])((?:(?!\1)[^\\]|\\.)*)\1/gi;
while ((match = styleRegex.exec(snapshotContent)) !== null) {
var styleValue = match[2];
if (styleValue && styleValue !== 'REDACTED') {
issues.push("style=\"".concat(styleValue, "\""));
}
}
// Check for style blocks (unless they contain "REDACTED")
var styleBlockRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
while ((match = styleBlockRegex.exec(snapshotContent)) !== null) {
var styleContent = match[1];
if (styleContent && !styleContent.trim().includes('REDACTED')) {
issues.push('style block');
}
}
return {
hasIssues: issues.length > 0,
issues: issues
};
}
/**
* Extracts the snapshot content from a template literal or string literal
*/
function extractSnapshotContent(node, sourceCode) {
if (isNodeOfType(node, 'TemplateLiteral')) {
// For template literals, get the raw text including the template parts
return sourceCode.getText(node);
}
if (isNodeOfType(node, 'Literal') && typeof node.value === 'string') {
return node.value;
}
return null;
}
var rule = createLintRule({
meta: {
name: name,
type: 'problem',
docs: {
description: 'Enforce guardrails on toMatchInlineSnapshot usage: snapshots must not exceed 100 lines and must not contain internal implementation details like className or style attributes.',
recommended: false,
severity: 'error'
},
messages: {
exceedsMaxLines: "Inline snapshot exceeds ".concat(MAX_LINES, " lines. Consider breaking it into smaller snapshots or using a different testing approach."),
containsInternalDetails: 'Inline snapshot contains internal implementation details: {{details}}. Use "REDACTED" for className and style values, or remove these details from the snapshot.'
}
},
create: function create(context) {
var sourceCode = getSourceCode(context);
return {
MemberExpression: function MemberExpression(node) {
// Check if this is a call to toMatchInlineSnapshot
if (!isNodeOfType(node.property, 'Identifier') || node.property.name !== 'toMatchInlineSnapshot') {
return;
}
// Check if the object is an expect() call
if (!isNodeOfType(node.object, 'CallExpression') || !isNodeOfType(node.object.callee, 'Identifier') || node.object.callee.name !== 'expect') {
return;
}
// Only report if this is being called (i.e., it's part of a CallExpression)
if (!node.parent || !isNodeOfType(node.parent, 'CallExpression')) {
return;
}
// Get the snapshot content from the first argument
var callExpression = node.parent;
if (callExpression.arguments.length === 0) {
return;
}
var snapshotArg = callExpression.arguments[0];
var snapshotContent = extractSnapshotContent(snapshotArg, sourceCode);
if (!snapshotContent) {
return;
}
// Check line count
var lines = snapshotContent.split('\n');
if (lines.length > MAX_LINES) {
context.report({
node: snapshotArg,
messageId: 'exceedsMaxLines'
});
return;
}
// Check for internal implementation details
var _containsInternalDeta = containsInternalDetails(snapshotContent),
hasIssues = _containsInternalDeta.hasIssues,
issues = _containsInternalDeta.issues;
if (hasIssues) {
context.report({
node: snapshotArg,
messageId: 'containsInternalDetails',
data: {
details: issues.slice(0, 3).join(', ') // Show first 3 issues
}
});
}
}
};
}
});
export default rule;