Skip to content

Commit

Permalink
feat: enforce fully optional expression chains @W-14387292
Browse files Browse the repository at this point in the history
  • Loading branch information
seckardt committed Nov 1, 2023
1 parent e24e8c0 commit d5059bc
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 7 deletions.
60 changes: 54 additions & 6 deletions lib/rule-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ const noReferenceParentQualifiers = new Set([
'AssignmentExpression',
]);

const globalAccessQualifiers = new Set(['CallExpression', 'MemberExpression']);

function inModuleScope(node, context) {
for (const ancestor of context.getAncestors()) {
if (moduleScopeDisqualifiers.has(ancestor.type)) {
Expand Down Expand Up @@ -245,26 +247,72 @@ module.exports.noPropertyAccessDuringSSR = function noPropertyAccessDuringSSR(
) {
const { withinLWCVisitors, isInsideReachableMethod, isInsideSkippedBlock } =
reachableDuringSSRPartial();
let expressionStatementWeAreIn = null;
let memberExpressionsInStatement = [];
let callExpressionsInStatement = [];

return {
...withinLWCVisitors,
ExpressionStatement: (node) => {
expressionStatementWeAreIn = node;
callExpressionsInStatement = [];
memberExpressionsInStatement = [];
},
'ExpressionStatement:exit': (node) => {
if (expressionStatementWeAreIn === node) {
expressionStatementWeAreIn = null;
callExpressionsInStatement = [];
memberExpressionsInStatement = [];
}
},
AssignmentExpression: (node) => {
callExpressionsInStatement.push(node);
},
CallExpression: (node) => {
callExpressionsInStatement.push(node);
},
MemberExpression: (node) => {
if (!isInsideReachableMethod() || isInsideSkippedBlock()) {
return;
}

memberExpressionsInStatement.push(node);

if (
node.parent.type === 'CallExpression' &&
node.parent.optional !== true &&
node.object.type === 'ThisExpression' &&
globalAccessQualifiers.has(node.parent.type) &&
node.property.type === 'Identifier' &&
forbiddenPropertyNames.includes(node.property.name)
) {
// Prevents expressions like:
// this.addEventListener('click', () => { ... });
// this.dispatchEvent(new CustomEvent('myevent'));
// this.querySelector('button').addEventListener('click', ...);
// this.querySelector?.('button').addEventListener('click', ...);
// this.querySelector?.('button')?.addEventListener('click', ...);
// this.querySelector?.('button').firstElementChild.id;
// this.childNodes.item(0).textContent = 'foo';

// Allows expressions like:
// this.addEventListener?.('click', () => { ... });
reporter(node);
// Allows all-optional expressions like:
// this.dispatchEvent?.(new CustomEvent('myevent'));
// this.querySelector?.('button')?.addEventListener?.('click', ...);
// this.querySelector?.('button')?.firstElementChild.id;
const allCallExpressionsOptional = callExpressionsInStatement.every(
(expression) => expression.optional,
);
const allMemberExpressionsOptional = memberExpressionsInStatement.every(
(expression, index) => {
if (expression.parent && expression.parent.type === 'CallExpression') {
// Skip CallExpressions here as they are treated separately
return true;
}
// Return `true` if the MemberExpression is either `optional` or
// the last expression of the chain (which is in revered order).
return expression.optional || index === 0;
},
);
if (!allCallExpressionsOptional || !allMemberExpressionsOptional) {
reporter(node);
}
}
},
};
Expand Down
113 changes: 112 additions & 1 deletion test/lib/rules/no-unsupported-ssr-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,22 @@ tester.run('no-unsupported-ssr-properties', rule, {
export default class Foo extends LightningElement {
connectedCallback() {
this.querySelector?.('span')?.foo();
this.querySelector?.('span').firstElementChild;
this.querySelector?.('span')?.firstElementChild;
this.querySelector?.('span')?.firstElementChild.id;
this.querySelector?.('span')?.firstElementChild?.id;
this.querySelector?.('span')?.firstElementChild?.id.length;
this.querySelector?.('span')?.firstElementChild?.id?.length;
this.querySelector?.('span')?.children.item?.(0);
this.querySelector?.('span')?.children?.item?.(0);
this.querySelector?.('span').getAttribute?.('role');
this.querySelector?.('span')?.getAttribute?.('role');
this.querySelector?.('span')?.getAttribute?.('role').length;
this.querySelector?.('span')?.getAttribute?.('role')?.length;
this.querySelector?.('span')?.getAttribute?.('role').includes?.('button');
this.querySelector?.('span')?.getAttribute?.('role')?.includes?.('button');
}
}
`,
Expand Down Expand Up @@ -265,5 +280,101 @@ tester.run('no-unsupported-ssr-properties', rule, {
},
],
},
{
code: `
import { LightningElement } from 'lwc';
export default class Foo extends LightningElement {
connectedCallback() {
this.querySelector?.('span').foo();
}
}
`,
errors: [
{
messageId: 'propertyAccessFound',
},
],
},
{
code: `
import { LightningElement } from 'lwc';
export default class Foo extends LightningElement {
connectedCallback() {
this.querySelector?.('span')?.getAttribute('role');
}
}
`,
errors: [
{
messageId: 'propertyAccessFound',
},
],
},
{
code: `
import { LightningElement } from 'lwc';
export default class Foo extends LightningElement {
connectedCallback() {
this.querySelector?.('span').foo.bar;
}
}
`,
errors: [
{
messageId: 'propertyAccessFound',
},
],
},
{
code: `
import { LightningElement } from 'lwc';
export default class Foo extends LightningElement {
connectedCallback() {
this.querySelector?.('span').getAttribute('role');
}
}
`,
errors: [
{
messageId: 'propertyAccessFound',
},
],
},
{
code: `
import { LightningElement } from 'lwc';
export default class Foo extends LightningElement {
connectedCallback() {
this.querySelector?.('span').getAttribute?.('role').startsWith('button');
}
}
`,
errors: [
{
messageId: 'propertyAccessFound',
},
],
},
{
code: `
import { LightningElement } from 'lwc';
export default class Foo extends LightningElement {
connectedCallback() {
this.childNodes.item(0).textContent = 'foo';
}
}
`,
errors: [
{
messageId: 'propertyAccessFound',
},
],
},
],
});

0 comments on commit d5059bc

Please sign in to comment.