@lipemat/eslint-config 5.0.0-beta.2 → 5.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lipemat/eslint-config",
3
- "version": "5.0.0-beta.2",
3
+ "version": "5.0.0-beta.3",
4
4
  "license": "MIT",
5
5
  "description": "Eslint configuration for all @lipemat packages",
6
6
  "engines": {
@@ -52,7 +52,7 @@ const plugin = {
52
52
  node,
53
53
  messageId: 'dangerousInnerHtml',
54
54
  fix: (fixer) => {
55
- return fixer.replaceText(node, `dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(${context.sourceCode.getText(htmlValue)})}}`);
55
+ return fixer.replaceText(node, `dangerouslySetInnerHTML={{__html: DOMPurify.sanitize( ${context.sourceCode.getText(htmlValue)} )}}`);
56
56
  },
57
57
  });
58
58
  },
@@ -1,7 +1,8 @@
1
1
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
- import { isSanitized } from '../utils/shared.js';
2
+ import { isDomElementType, isSafeLiteralString, isSanitized } from '../utils/shared.js';
3
3
  const UNSAFE_PROPERTIES = [
4
- 'innerHTML', 'outerHTML',
4
+ 'innerHTML',
5
+ 'outerHTML',
5
6
  ];
6
7
  function isUnsafeProperty(propertyName) {
7
8
  return UNSAFE_PROPERTIES.includes(propertyName);
@@ -26,8 +27,7 @@ const plugin = {
26
27
  create(context) {
27
28
  return {
28
29
  AssignmentExpression(node) {
29
- // Handle element.innerHTML = value and element.outerHTML = value
30
- if (AST_NODE_TYPES.MemberExpression !== node.left.type || !('name' in node.left.property)) {
30
+ if (AST_NODE_TYPES.MemberExpression !== node.left.type || AST_NODE_TYPES.Identifier !== node.left.property.type) {
31
31
  return;
32
32
  }
33
33
  const propertyName = node.left.property.name;
@@ -35,7 +35,7 @@ const plugin = {
35
35
  return;
36
36
  }
37
37
  const value = node.right;
38
- if (!isSanitized(value)) {
38
+ if (!isSafeLiteralString(value) && !isSanitized(value) && isDomElementType(node.left.object, context)) {
39
39
  context.report({
40
40
  node,
41
41
  messageId: 'executed',
@@ -47,14 +47,14 @@ const plugin = {
47
47
  messageId: 'domPurify',
48
48
  fix: (fixer) => {
49
49
  const valueText = context.sourceCode.getText(value);
50
- return fixer.replaceText(value, `DOMPurify.sanitize(${valueText})`);
50
+ return fixer.replaceText(value, `DOMPurify.sanitize( ${valueText} )`);
51
51
  },
52
52
  },
53
53
  {
54
54
  messageId: 'sanitize',
55
55
  fix: (fixer) => {
56
56
  const valueText = context.sourceCode.getText(value);
57
- return fixer.replaceText(value, `sanitize(${valueText})`);
57
+ return fixer.replaceText(value, `sanitize( ${valueText} )`);
58
58
  },
59
59
  },
60
60
  ],
@@ -1,5 +1,5 @@
1
1
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
- import { isDomElementType, isSanitized } from '../utils/shared.js';
2
+ import { isDomElementType, isSafeLiteralString, isSanitized } from '../utils/shared.js';
3
3
  import { isJQueryCall } from './jquery-executing.js';
4
4
  const DOCUMENT_METHODS = [
5
5
  'document.write',
@@ -42,19 +42,20 @@ function getDocumentCall(node) {
42
42
  }
43
43
  return null;
44
44
  }
45
- function getElementMethodCall(node) {
46
- // Detect element.method(userInput) calls
47
- if (AST_NODE_TYPES.MemberExpression !== node.callee.type || !('name' in node.callee.property)) {
45
+ function getElementMethodCall(node, context) {
46
+ if (AST_NODE_TYPES.MemberExpression !== node.callee.type || AST_NODE_TYPES.Identifier !== node.callee.property.type) {
48
47
  return null;
49
48
  }
50
49
  const methodName = node.callee.property.name;
50
+ if (!isDomElementType(node.callee.object, context)) {
51
+ return null; // We only care about DOM element method calls.
52
+ }
51
53
  if (!isUnsafeMethod(methodName)) {
52
54
  return null;
53
55
  }
54
56
  if (isJQueryCall(node)) {
55
57
  return null; // Handled in jquery-executing rule
56
58
  }
57
- // This is a generic element method call, not jQuery specific
58
59
  return methodName;
59
60
  }
60
61
  const plugin = {
@@ -91,7 +92,7 @@ const plugin = {
91
92
  method = documentMethod;
92
93
  }
93
94
  else {
94
- method = getElementMethodCall(node);
95
+ method = getElementMethodCall(node, context);
95
96
  if (null === method) {
96
97
  return;
97
98
  }
@@ -100,7 +101,7 @@ const plugin = {
100
101
  if (isSecondArgMethod(method)) {
101
102
  arg = node.arguments[1];
102
103
  }
103
- if (!isSanitized(arg) && !isDomElementType(arg, context)) {
104
+ if (!isSafeLiteralString(arg) && !isSanitized(arg) && !isDomElementType(arg, context)) {
104
105
  context.report({
105
106
  node,
106
107
  messageId: method,
@@ -109,14 +110,14 @@ const plugin = {
109
110
  messageId: 'domPurify',
110
111
  fix: (fixer) => {
111
112
  const argText = context.sourceCode.getText(arg);
112
- return fixer.replaceText(arg, `DOMPurify.sanitize(${argText})`);
113
+ return fixer.replaceText(arg, `DOMPurify.sanitize( ${argText} )`);
113
114
  },
114
115
  },
115
116
  {
116
117
  messageId: 'sanitize',
117
118
  fix: (fixer) => {
118
119
  const argText = context.sourceCode.getText(arg);
119
- return fixer.replaceText(arg, `sanitize(${argText})`);
120
+ return fixer.replaceText(arg, `sanitize( ${argText} )`);
120
121
  },
121
122
  },
122
123
  ],
@@ -1,11 +1,5 @@
1
1
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
- import { isSanitized, isStringLike } from '../utils/shared.js';
3
- /**
4
- * Check if a node is a literal string
5
- */
6
- function isLiteralString(node) {
7
- return AST_NODE_TYPES.Literal === node.type && 'string' === typeof node.value;
8
- }
2
+ import { isLiteralString, isSanitized, isStringLike } from '../utils/shared.js';
9
3
  /**
10
4
  * Get the callee name from a call expression
11
5
  */
@@ -87,11 +81,11 @@ const plugin = {
87
81
  suggest: [
88
82
  {
89
83
  messageId: 'domPurify',
90
- fix: fixer => fixer.replaceText(firstArg, `DOMPurify.sanitize(${argText})`),
84
+ fix: fixer => fixer.replaceText(firstArg, `DOMPurify.sanitize( ${argText} )`),
91
85
  },
92
86
  {
93
87
  messageId: 'sanitize',
94
- fix: fixer => fixer.replaceText(firstArg, `sanitize(${argText})`),
88
+ fix: fixer => fixer.replaceText(firstArg, `sanitize( ${argText} )`),
95
89
  },
96
90
  ],
97
91
  });
@@ -111,11 +105,11 @@ const plugin = {
111
105
  suggest: [
112
106
  {
113
107
  messageId: 'domPurify',
114
- fix: fixer => fixer.replaceText(node.right, `DOMPurify.sanitize(${rightText})`),
108
+ fix: fixer => fixer.replaceText(node.right, `DOMPurify.sanitize( ${rightText} )`),
115
109
  },
116
110
  {
117
111
  messageId: 'sanitize',
118
- fix: fixer => fixer.replaceText(node.right, `sanitize(${rightText})`),
112
+ fix: fixer => fixer.replaceText(node.right, `sanitize( ${rightText} )`),
119
113
  },
120
114
  ],
121
115
  });
@@ -3,7 +3,12 @@ function isStringConcat(node) {
3
3
  // 'foo' + userInput + 'bar' (HTML-like only)
4
4
  return AST_NODE_TYPES.BinaryExpression === node.type && '+' === node.operator && hasHtmlLikeStrings(node);
5
5
  }
6
- function hasHtmlLikeStrings(node) {
6
+ /**
7
+ * Check if an expression contains any HTML-like strings.
8
+ * - Looks for `<` or `>` characters in string literals and template literals.
9
+ * - Recursively checks binary expressions with the ` + ` operator.
10
+ */
11
+ export function hasHtmlLikeStrings(node) {
7
12
  if (AST_NODE_TYPES.Literal === node.type && 'string' === typeof node.value) {
8
13
  return /[<>]/.test(node.value);
9
14
  }
@@ -84,14 +84,14 @@ const plugin = {
84
84
  messageId: 'domPurify',
85
85
  fix: (fixer) => {
86
86
  const argText = context.sourceCode.getText(arg);
87
- return fixer.replaceText(arg, `DOMPurify.sanitize(${argText})`);
87
+ return fixer.replaceText(arg, `DOMPurify.sanitize( ${argText} )`);
88
88
  },
89
89
  },
90
90
  {
91
91
  messageId: 'sanitize',
92
92
  fix: (fixer) => {
93
93
  const argText = context.sourceCode.getText(arg);
94
- return fixer.replaceText(arg, `sanitize(${argText})`);
94
+ return fixer.replaceText(arg, `sanitize( ${argText} )`);
95
95
  },
96
96
  },
97
97
  ],
@@ -1,5 +1,5 @@
1
1
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
- import { isSanitized } from '../utils/shared.js';
2
+ import { isLiteralString, isSanitized } from '../utils/shared.js';
3
3
  // Window and location properties that need special handling
4
4
  const LOCATION_PROPS = new Set(['href', 'src', 'action',
5
5
  'protocol', 'host', 'hostname', 'pathname', 'search', 'hash', 'username', 'port', 'name', 'status',
@@ -9,37 +9,7 @@ export function isSafeUrlString(value) {
9
9
  return !/^\s*(?:javascript|data|vbscript|about|livescript)\s*:/i.test(decodeURIComponent(value.replace(/[\u0000-\u001F\u007F]+/g, '')));
10
10
  }
11
11
  function isSafeUrlLiteral(node) {
12
- if (AST_NODE_TYPES.TemplateElement !== node.type && AST_NODE_TYPES.Literal !== node.type) {
13
- return false;
14
- }
15
- if (typeof node.value !== 'string') {
16
- return false;
17
- }
18
- return isSafeUrlString(node.value);
19
- }
20
- function isSafeUrlTemplate(node) {
21
- if (AST_NODE_TYPES.TemplateLiteral !== node.type || 0 === node.quasis.length) {
22
- return false;
23
- }
24
- // Basic scheme safety on the first static chunk
25
- const firstChunk = node.quasis[0];
26
- if (isSafeUrlLiteral(firstChunk)) {
27
- return true;
28
- }
29
- return isUrlEncoded(node);
30
- }
31
- function isUrlEncoded(node) {
32
- if (AST_NODE_TYPES.TemplateLiteral !== node.type) {
33
- return false;
34
- }
35
- return Array.isArray(node.expressions) && node.expressions.length > 0 && node.expressions.every(isEncoded);
36
- }
37
- function isEncoded(node) {
38
- if (AST_NODE_TYPES.CallExpression !== node.type) {
39
- return false;
40
- }
41
- return AST_NODE_TYPES.Identifier === node.callee.type &&
42
- ('encodeURIComponent' === node.callee.name || 'encodeURI' === node.callee.name);
12
+ return isLiteralString(node) && isSafeUrlString(node.value);
43
13
  }
44
14
  function isWindowLocationAssignment(node) {
45
15
  // window.location.<prop> = ...
@@ -60,16 +30,16 @@ function isWindowAssignment(node) {
60
30
  'window' === node.left.object.name &&
61
31
  WINDOW_PROPS.has(node.left.property.name));
62
32
  }
63
- function isWindowOrLocationMemberExpression(memberExpr) {
33
+ function isWindowOrLocation(expression) {
64
34
  // Helper to detect a window.* or window.location.*
65
- if (AST_NODE_TYPES.MemberExpression !== memberExpr.type) {
35
+ if (AST_NODE_TYPES.MemberExpression !== expression.type) {
66
36
  return false;
67
37
  }
68
- if (AST_NODE_TYPES.Identifier === memberExpr.object.type && 'window' === memberExpr.object.name) {
38
+ if (AST_NODE_TYPES.Identifier === expression.object.type && 'window' === expression.object.name) {
69
39
  return true;
70
40
  }
71
- if (AST_NODE_TYPES.MemberExpression === memberExpr.object.type) {
72
- const memberObject = memberExpr.object;
41
+ if (AST_NODE_TYPES.MemberExpression === expression.object.type) {
42
+ const memberObject = expression.object;
73
43
  const isObjectWindow = AST_NODE_TYPES.Identifier === memberObject.object.type && 'window' === memberObject.object.name;
74
44
  const isPropertyLocation = AST_NODE_TYPES.Identifier === memberObject.property.type && 'location' === memberObject.property.name;
75
45
  return isObjectWindow && isPropertyLocation;
@@ -108,10 +78,7 @@ const plugin = {
108
78
  if (!LOCATION_PROPS.has(propName)) {
109
79
  return;
110
80
  }
111
- if (isSafeUrlLiteral(rhsResolved) || isSafeUrlTemplate(rhsResolved)) {
112
- return;
113
- }
114
- if (isSanitized(rhsResolved)) {
81
+ if (isSafeUrlLiteral(rhsResolved) || isSanitized(rhsResolved)) {
115
82
  return;
116
83
  }
117
84
  context.report({
@@ -125,13 +92,13 @@ const plugin = {
125
92
  messageId: 'sanitize',
126
93
  fix: (fixer) => {
127
94
  const argText = context.sourceCode.getText(right);
128
- return fixer.replaceText(right, `sanitize(${argText})`);
95
+ return fixer.replaceText(right, `sanitize( ${argText} )`);
129
96
  },
130
97
  }, {
131
98
  messageId: 'domPurify',
132
99
  fix: (fixer) => {
133
100
  const argText = context.sourceCode.getText(right);
134
- return fixer.replaceText(right, `DOMPurify.sanitize(${argText})`);
101
+ return fixer.replaceText(right, `DOMPurify.sanitize( ${argText} )`);
135
102
  },
136
103
  },
137
104
  ],
@@ -154,13 +121,13 @@ const plugin = {
154
121
  messageId: 'sanitize',
155
122
  fix: (fixer) => {
156
123
  const argText = context.sourceCode.getText(right);
157
- return fixer.replaceText(right, `sanitize(${argText})`);
124
+ return fixer.replaceText(right, `sanitize( ${argText} )`);
158
125
  },
159
126
  }, {
160
127
  messageId: 'domPurify',
161
128
  fix: (fixer) => {
162
129
  const argText = context.sourceCode.getText(right);
163
- return fixer.replaceText(right, `DOMPurify.sanitize(${argText})`);
130
+ return fixer.replaceText(right, `DOMPurify.sanitize( ${argText} )`);
164
131
  },
165
132
  },
166
133
  ],
@@ -173,7 +140,7 @@ const plugin = {
173
140
  if (AST_NODE_TYPES.AssignmentExpression === parent.type && parent.left === node) {
174
141
  return;
175
142
  }
176
- if (!isWindowOrLocationMemberExpression(node) || !('name' in node.property)) {
143
+ if (!isWindowOrLocation(node) || !('name' in node.property)) {
177
144
  return;
178
145
  }
179
146
  const propName = node.property.name;
@@ -1,5 +1,12 @@
1
1
  import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
2
2
  import {} from 'typescript';
3
+ /**
4
+ * Is the node of type string.
5
+ * - String literals.
6
+ * - constants of type string.
7
+ * - template literals.
8
+ * - intrinsic type string.
9
+ */
3
10
  export function isStringLike(node, context) {
4
11
  const type = getType(node, context);
5
12
  const literal = type.isStringLiteral();
@@ -9,9 +16,9 @@ export function isStringLike(node, context) {
9
16
  /**
10
17
  * Get the TypeScript type of node.
11
18
  */
12
- export function getType(arg, context) {
19
+ export function getType(expression, context) {
13
20
  const { getTypeAtLocation } = ESLintUtils.getParserServices(context);
14
- const type = getTypeAtLocation(arg);
21
+ const type = getTypeAtLocation(expression);
15
22
  return type.getNonNullableType();
16
23
  }
17
24
  /**
@@ -22,10 +29,13 @@ export function getType(arg, context) {
22
29
  *
23
30
  * @link https://typescript-eslint.io/developers/custom-rules/#typed-rules
24
31
  */
25
- export function isDomElementType(arg, context) {
26
- const element = getType(arg, context);
27
- const name = element.getSymbol()?.escapedName ?? '';
32
+ export function isDomElementType(expression, context) {
33
+ const type = getType(expression, context);
34
+ const name = type.getSymbol()?.escapedName ?? '';
28
35
  // Match any type that ends with "Element", e.g., HTMLElement, HTMLDivElement, Element, etc.
36
+ if ('Element' === name) {
37
+ return true;
38
+ }
29
39
  return name.startsWith('HTML') && name.endsWith('Element');
30
40
  }
31
41
  /**
@@ -45,3 +55,24 @@ export function isSanitized(node) {
45
55
  }
46
56
  return false;
47
57
  }
58
+ /**
59
+ * Check if a node is a literal string
60
+ */
61
+ export function isLiteralString(node) {
62
+ return AST_NODE_TYPES.Literal === node.type && 'string' === typeof node.value;
63
+ }
64
+ /**
65
+ * Check if a node is a literal string that is safe to use in an HTML context.
66
+ * - Must be a literal string.
67
+ * - Must not contain `<script`.
68
+ * - Must not start with a dangerous protocol (javascript:, data:, vbscript:, about:, livescript:).
69
+ */
70
+ export function isSafeLiteralString(node) {
71
+ if (!isLiteralString(node)) {
72
+ return false;
73
+ }
74
+ if (node.value.includes('<script')) {
75
+ return false;
76
+ }
77
+ return !/^\s*(?:javascript|data|vbscript|about|livescript)\s*:/i.test(decodeURIComponent(node.value.replace(/[\u0000-\u001F\u007F]+/g, '')));
78
+ }
@@ -1,3 +1,9 @@
1
- import { type TSESLint } from '@typescript-eslint/utils';
1
+ import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
+ /**
3
+ * Check if an expression contains any HTML-like strings.
4
+ * - Looks for `<` or `>` characters in string literals and template literals.
5
+ * - Recursively checks binary expressions with the ` + ` operator.
6
+ */
7
+ export declare function hasHtmlLikeStrings(node: TSESTree.Expression | TSESTree.PrivateIdentifier): boolean;
2
8
  declare const plugin: TSESLint.RuleModule<'htmlStringConcat'>;
3
9
  export default plugin;
@@ -1,10 +1,17 @@
1
1
  import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
2
  import { type Type } from 'typescript';
3
+ /**
4
+ * Is the node of type string.
5
+ * - String literals.
6
+ * - constants of type string.
7
+ * - template literals.
8
+ * - intrinsic type string.
9
+ */
3
10
  export declare function isStringLike(node: TSESTree.CallExpressionArgument, context: Readonly<TSESLint.RuleContext<string, readonly []>>): boolean;
4
11
  /**
5
12
  * Get the TypeScript type of node.
6
13
  */
7
- export declare function getType<Context extends Readonly<TSESLint.RuleContext<string, readonly []>>>(arg: TSESTree.CallExpressionArgument, context: Context): Type;
14
+ export declare function getType<Context extends Readonly<TSESLint.RuleContext<string, readonly []>>>(expression: TSESTree.CallExpressionArgument, context: Context): Type;
8
15
  /**
9
16
  * Is the type of variable being passed a DOM element?
10
17
  *
@@ -13,9 +20,20 @@ export declare function getType<Context extends Readonly<TSESLint.RuleContext<st
13
20
  *
14
21
  * @link https://typescript-eslint.io/developers/custom-rules/#typed-rules
15
22
  */
16
- export declare function isDomElementType<Context extends Readonly<TSESLint.RuleContext<string, readonly []>>>(arg: TSESTree.CallExpressionArgument, context: Context): boolean;
23
+ export declare function isDomElementType<Context extends Readonly<TSESLint.RuleContext<string, readonly []>>>(expression: TSESTree.CallExpressionArgument, context: Context): boolean;
17
24
  /**
18
25
  * Check if a node is a call to a known sanitization function.
19
26
  * - Currently recognizes `sanitize(...)` and `DOMPurify.sanitize(...)`.
20
27
  */
21
28
  export declare function isSanitized(node: TSESTree.Property['value'] | TSESTree.CallExpressionArgument): boolean;
29
+ /**
30
+ * Check if a node is a literal string
31
+ */
32
+ export declare function isLiteralString(node: TSESTree.Property['value'] | TSESTree.CallExpressionArgument): node is TSESTree.StringLiteral;
33
+ /**
34
+ * Check if a node is a literal string that is safe to use in an HTML context.
35
+ * - Must be a literal string.
36
+ * - Must not contain `<script`.
37
+ * - Must not start with a dangerous protocol (javascript:, data:, vbscript:, about:, livescript:).
38
+ */
39
+ export declare function isSafeLiteralString(node: TSESTree.Property['value'] | TSESTree.CallExpressionArgument): boolean;