Skip to content

Commit

Permalink
[ESLint] Improve handling of resolving local constants (facebook#6)
Browse files Browse the repository at this point in the history
* Handle 'as' and 'satisfies' expressions for style values
  • Loading branch information
nmn authored Nov 22, 2023
1 parent 289cb7c commit 92b8767
Show file tree
Hide file tree
Showing 10 changed files with 120 additions and 36 deletions.
3 changes: 1 addition & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@
"coverage",
"dist",
"lib",
"node_modules",
"nextjs-example"
"node_modules"
],
"globals": {
"$Call": "readonly",
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
with:
node-version: '18.x'
- run: npm install
- run: npm run build
- run: npm run lint:report

unit-test:
Expand Down
3 changes: 3 additions & 0 deletions apps/nextjs-example/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ module.exports = {
// The Eslint rule still needs work, but you can
// enable it to test things out.
'@stylexjs/valid-styles': 'error',
'ft-flow/space-after-type-colon': 0,
'ft-flow/no-types-missing-file-annotation': 0,
'ft-flow/generic-spacing': 0,
},
};
10 changes: 8 additions & 2 deletions apps/nextjs-example/app/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ export default function Card({ title, body, href }: Props) {
);
}

const MOBILE = '@media (max-width: 700px)' as const;
type TMobile = '@media (max-width: 700px)';

const MOBILE: TMobile = '@media (max-width: 700px)' as TMobile;
const REDUCE_MOTION = '@media (prefers-reduced-motion: reduce)' as const;

const bgDefault = `rgba(${$.cardR}, ${$.cardG}, ${$.cardB}, 0)` as const;

const styles = stylex.create({
link: {
display: {
Expand All @@ -47,7 +51,7 @@ const styles = stylex.create({
flexDirection: 'column',
borderRadius: spacing.xs,
backgroundColor: {
default: `rgba(${$.cardR}, ${$.cardG}, ${$.cardB}, 0)`,
default: bgDefault,
':hover': `rgba(${$.cardR}, ${$.cardG}, ${$.cardB}, 0.1)`,
},
borderWidth: 1,
Expand Down Expand Up @@ -93,4 +97,6 @@ const styles = stylex.create({
lineHeight: 1.5,
maxWidth: '30ch',
},
color: (color: string) => ({ color }),
width: (width: string) => ({ width }),
});
2 changes: 2 additions & 0 deletions apps/nextjs-example/typetests/typetests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ const styles8: Readonly<{
}>;
}> = stylex.create({
foo: {
// In a real example `vars` would be imported from another file.
// eslint-disable-next-line @stylexjs/valid-styles
color: vars.accent,
},
});
Expand Down
3 changes: 3 additions & 0 deletions packages/eslint-plugin/src/rules/isAnimationName.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export default function isAnimationName(
}
if (node.type === 'Identifier' && variables && variables.has(node.name)) {
const variable = variables.get(node.name);
if (variable === 'ARG') {
return undefined;
}
if (variable != null) {
return isAnimationName(variable, variables);
} else {
Expand Down
13 changes: 1 addition & 12 deletions packages/eslint-plugin/src/rules/makeUnionRule.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,6 @@ export default function makeUnionRule(
variables?: Variables,
prop?: Property,
): RuleResponse => {
let isBorder = false;
if (
prop != null &&
prop.key.type === 'Identifier' &&
prop.key.name === 'border'
) {
console.log('UNION OF BORDER', prop);
isBorder = true;
}
const failedRules = [];
for (const _rule of rules) {
const rule =
Expand All @@ -50,10 +41,8 @@ export default function makeUnionRule(
failedRules.push(check);
}
const fixable = failedRules.filter((a) => a.suggest != null);
if (isBorder) {
console.log('UNION OF BORDER fixable Rules:', fixable, node);
}
fixable.sort((a, b) => (a.distance || Infinity) - (b.distance || Infinity));

return {
message: failedRules.map((a) => a.message).join('\n'),
suggest: fixable[0] != null ? fixable[0].suggest : undefined,
Expand Down
63 changes: 45 additions & 18 deletions packages/eslint-plugin/src/stylex-valid-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ import isPercentage from './rules/isPercentage';
import isAnimationName from './rules/isAnimationName';
import isStylexDefineVarsToken from './rules/isStylexDefineVarsToken';
import { borderSplitter } from './utils/split-css-value';
import evaluate from './utils/evaluate';

export type Variables = $ReadOnlyMap<string, Expression>;
export type Variables = $ReadOnlyMap<string, Expression | 'ARG'>;
export type RuleCheck = (
node: $ReadOnly<Expression | Pattern>,
variables?: $ReadOnly<Variables>,
variables?: Variables,
prop?: $ReadOnly<Property>,
) => RuleResponse;
export type RuleResponse = void | {
Expand Down Expand Up @@ -2232,7 +2233,7 @@ const stylexValidStyles = {
],
},
create(context: Rule.RuleContext): { ... } {
const variables = new Map<string, Expression>();
const variables = new Map<string, Expression | 'ARG'>();
const dynamicStyleVariables = new Set<string>();

const legacyReason =
Expand Down Expand Up @@ -2424,20 +2425,35 @@ const stylexValidStyles = {
);
}
let styleKey: Expression | PrivateIdentifier = style.key;
if (style.computed && style.key.type !== 'Literal') {
if (style.key.type === 'Identifier') {
const varValue = variables.get(style.key.name);
if (varValue != null) {
styleKey = varValue;
} else {
return context.report(
({
node: style.key,
loc: style.key.loc,
message: 'Computed key cannot be resolved.',
}: Rule.ReportDescriptor),
);
}
if (styleKey.type === 'PrivateIdentifier') {
return context.report(
({
node: styleKey,
loc: styleKey.loc,
message: 'Private properties are not allowed in stylex',
}: Rule.ReportDescriptor),
);
}
if (style.computed && styleKey.type !== 'Literal') {
const val = evaluate(styleKey, variables);
if (val == null) {
return context.report(
({
node: style.key,
loc: style.key.loc,
message: 'Computed key cannot be resolved.',
}: Rule.ReportDescriptor),
);
} else if (val === 'ARG') {
return context.report(
({
node: style.key,
loc: style.key.loc,
message: 'Computed key cannot depend on function argument',
}: Rule.ReportDescriptor),
);
} else {
styleKey = val;
}
}
if (styleKey.type !== 'Literal' && styleKey.type !== 'Identifier') {
Expand Down Expand Up @@ -2531,7 +2547,18 @@ const stylexValidStyles = {
stylexDefineVarsTokenImports.size > 0 &&
isStylexDefineVarsToken(style.value, stylexDefineVarsTokenImports);
if (!isReferencingStylexDefineVarsTokens) {
const check = ruleChecker(style.value, variables, style);
let varsWithFnArgs: Map<string, Expression | 'ARG'> = variables;
if (dynamicStyleVariables.size > 0) {
varsWithFnArgs = new Map();
for (const [key, value] of variables) {
varsWithFnArgs.set(key, value);
}
for (const key of dynamicStyleVariables) {
varsWithFnArgs.set(key, 'ARG');
}
}

const check = ruleChecker(style.value, varsWithFnArgs, style);
if (check != null) {
const { message, suggest } = check;
return context.report(
Expand Down
38 changes: 38 additions & 0 deletions packages/eslint-plugin/src/utils/evaluate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/

import type { Expression, Pattern, Literal } from 'estree';
import type { Variables } from '../stylex-valid-styles';

export default function evaluate(
node: Expression | Pattern,
variables?: Variables,
): null | Literal | 'ARG' {
if (
// $FlowFixMe
node.type === 'TSSatisfiesExpression' ||
// $FlowFixMe
node.type === 'TSAsExpression'
) {
return evaluate(node.expression, variables);
}
if (node.type === 'Identifier' && variables != null) {
const existingVar = variables.get(node.name);
if (existingVar === 'ARG') {
return 'ARG';
}
if (existingVar != null) {
return evaluate(existingVar, variables);
}
}
if (node.type === 'Literal') {
return node;
}
return null;
}
20 changes: 18 additions & 2 deletions packages/eslint-plugin/src/utils/makeVariableCheckingRule.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,29 @@ import type {
} from '../stylex-valid-styles';

export default function makeVariableCheckingRule(rule: RuleCheck): RuleCheck {
return (node: Expression | Pattern, variables?: Variables): RuleResponse => {
const varCheckingRule = (
node: Expression | Pattern,
variables?: Variables,
): RuleResponse => {
if (
// $FlowFixMe
node.type === 'TSSatisfiesExpression' ||
// $FlowFixMe
node.type === 'TSAsExpression'
) {
return varCheckingRule(node.expression, variables);
}
if (node.type === 'Identifier' && variables != null) {
const existingVar = variables.get(node.name);
if (existingVar === 'ARG') {
return undefined;
}
if (existingVar != null) {
return rule(existingVar);
return varCheckingRule(existingVar, variables);
}
}
return rule(node);
};

return varCheckingRule;
}

0 comments on commit 92b8767

Please sign in to comment.