Skip to content

Commit

Permalink
fix(eslint): check for improper sanitize function usage
Browse files Browse the repository at this point in the history
  • Loading branch information
roberttran-cc committed Mar 17, 2023
1 parent c1d40a0 commit 1947db0
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 3 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = {
// custom rules
'i18n-always-arrow-with-sanitize': ['error'],
'i18n-always-sanitize-with-html': ['error'],
'i18n-always-template-literal-sanitize': ['error'],
'i18n-no-paramless-arrow': ['error'],
'i18n-no-sanitize-without-html': ['error'],
'i18n-order': ['error'],
Expand Down
7 changes: 6 additions & 1 deletion docs/translations.example.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const translations = {
return sanitize`<em>good</em> ${foo}`;
},

// The sanitize tag function is must always be used when the translation contains HTML!
// The sanitize tag function must always be used when the translation contains HTML!
// ESlint "translations-always-sanitize-with-html"
'cc-bad.html-no-sanitize': () => sanitize`<em>bad</em>`,
'cc-bad.html-no-sanitize-params': ({ foo }) => sanitize`<em>bad</em> ${foo}`,
Expand All @@ -125,4 +125,9 @@ export const translations = {
// The sanitize tag function must always be used in an arrow function!
// Enforced by ESlint rule "i18n-always-arrow-with-sanitize"
'cc-bad.sanitize-without-arrow': () => sanitize`<em>bad</em>`,

// The sanitize tag function must also be used as a template literal and not
// a regular function that returns a template literal (for security reasons).
// Enforced by ESlint rule "i18n-always-template-literal-sanitize"
'cc-bad.sanitize-without-template-literal': () => sanitize(`<em>bad</em>`),
};
67 changes: 67 additions & 0 deletions eslint-rules/i18n-always-template-literal-sanitize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Rule to enforce using `sanitize` function as a template literal and not as a regular function.
* Check the `docs/translations.example.js` file for more details.
*
* Note: this rule only applies in translation files.
*
* Limitations: the automatic fix script only affects `sanitize()` call with a single parameter,
* which must be a template literal (`TemplateLiteral` AST type).
*/

'use strict';

const {
getClosestParentFromType,
isTranslationFile,
} = require('./i18n-shared.js');

function report (context, key, callExpressionNode) {
context.report({
node: callExpressionNode,
messageId: 'sanitizeAlwaysTemplateLiteral',
data: { key },
fix: (fixer) => {
if (callExpressionNode.arguments?.length !== 1) {
return;
}

const argument = callExpressionNode.arguments[0];
if (argument.type === 'TemplateLiteral') {
const contents = context.getSourceCode().text.substring(argument.start, argument.end);
return fixer.replaceText(callExpressionNode, `sanitize${contents}`);
}
},
});
}

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce template literal when using the sanitize function',
category: 'Translation files',
},
fixable: 'code',
messages: {
sanitizeAlwaysTemplateLiteral: 'Missing template literal usage with sanitize function: {{key}}',
},
},
create: function (context) {

// Early return for non translation files
if (!isTranslationFile(context)) {
return {};
}

return {
CallExpression (node) {
if (node.callee.name === 'sanitize') {
const parentProperty = getClosestParentFromType(node, 'Property');
if (parentProperty != null) {
report(context, parentProperty.key.value, node);
}
}
},
};
},
};
25 changes: 25 additions & 0 deletions eslint-rules/i18n-shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,30 @@ function isLanguageTranslation (node) {
&& (node.name === 'LANGUAGE');
}

/**
* Returns the closest parent from a node that matches the specified type.
* If no type is specified, returns the direct parent.
* If no node is found, returns the root node (matching the 'Program' type).
* @param node
* @param {String|null} type
* @returns {*|null}
*/
function getClosestParentFromType (node, type) {
if (node == null) {
return null;
}
if (type == null) {
return node.parent;
}

let directParent = node;
do {
directParent = directParent.parent;
} while (directParent.parent?.type !== type && directParent.type !== 'Program');

return directParent.parent;
}

function getTranslationProperties (node) {
return node.declaration.declarations[0].init.properties
.filter((node) => !isLanguageTranslation(node.key));
Expand All @@ -47,6 +71,7 @@ function isSanitizeTagFunction (node) {
module.exports = {
isTranslationFile,
isMainTranslationNode,
getClosestParentFromType,
getTranslationProperties,
parseTemplate,
isSanitizeTagFunction,
Expand Down
2 changes: 1 addition & 1 deletion src/translations/translations.en.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export const translations = {
'cc-datetime-relative.title': ({ date }) => formatDate(lang, date),
//#endregion
//#region cc-doc-card
'cc-doc-card.link': ({ link, product }) => sanitize(`<a href=${link} aria-label="Read the documentation - ${product}">Read the documentation</a>`),
'cc-doc-card.link': ({ link, product }) => sanitize`<a href=${link} aria-label="Read the documentation - ${product}">Read the documentation</a>`,
'cc-doc-card.skeleton-link-title': `Read the documentation`,
//#endregion
//#region cc-doc-list
Expand Down
2 changes: 1 addition & 1 deletion src/translations/translations.fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export const translations = {
'cc-datetime-relative.title': ({ date }) => formatDate(lang, date),
//#endregion
//#region cc-doc-card
'cc-doc-card.link': ({ link, product }) => sanitize(`<a href=${link} aria-label="Lire la documentation - ${product}">Lire la documentation</a>`),
'cc-doc-card.link': ({ link, product }) => sanitize`<a href=${link} aria-label="Lire la documentation - ${product}">Lire la documentation</a>`,
'cc-doc-card.skeleton-link-title': `Lire la documentation`,
//#endregion
//#region cc-doc-list
Expand Down

0 comments on commit 1947db0

Please sign in to comment.