diff --git a/README.md b/README.md index c564f62..ace9204 100644 --- a/README.md +++ b/README.md @@ -105,15 +105,16 @@ To use ESLint with VSCode, see the [ESLint VSCode extension](https://marketplace 🔦 Set in the `recommended-problems-only` configuration.\ 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). -| Name | Description | 💼 | ⚠️ | 🔧 | -| :------------------------------------------------------------------------------------------------- | :--------------------------------------------------- | :---- | :- | :- | -| [await-requires-async](docs/rules/await-requires-async.md) | Require functions that contain `await` to be `async` | 👍 🔦 | | 🔧 | -| [ban-deprecated-id-params](docs/rules/ban-deprecated-id-params.md) | Ban use of deprecated string ID parameters | 👍 🔦 | | 🔧 | -| [ban-deprecated-sync-methods](docs/rules/ban-deprecated-sync-methods.md) | Ban use of deprecated synchronous methods | 👍 🔦 | | 🔧 | -| [ban-deprecated-sync-prop-getters](docs/rules/ban-deprecated-sync-prop-getters.md) | Ban use of deprecated synchronous property getters | 👍 🔦 | | 🔧 | -| [ban-deprecated-sync-prop-setters](docs/rules/ban-deprecated-sync-prop-setters.md) | Ban use of deprecated synchronous property getters | 👍 🔦 | | 🔧 | -| [dynamic-page-documentchange-event-advice](docs/rules/dynamic-page-documentchange-event-advice.md) | Advice on using the `documentchange` event | | 👍 | | -| [dynamic-page-find-method-advice](docs/rules/dynamic-page-find-method-advice.md) | Advice on using the find*() family of methods | | 👍 | | +| Name                                                         | Description | 💼 | ⚠️ | 🔧 | +| :----------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------- | :---- | :- | :- | +| [await-requires-async](docs/rules/await-requires-async.md) | Require functions that contain `await` to be `async` | 👍 🔦 | | 🔧 | +| [ban-deprecated-id-params](docs/rules/ban-deprecated-id-params.md) | Ban use of deprecated string ID parameters | 👍 🔦 | | 🔧 | +| [ban-deprecated-sync-methods](docs/rules/ban-deprecated-sync-methods.md) | Ban use of deprecated synchronous methods | 👍 🔦 | | 🔧 | +| [ban-deprecated-sync-prop-getters](docs/rules/ban-deprecated-sync-prop-getters.md) | Ban use of deprecated synchronous property getters | 👍 🔦 | | 🔧 | +| [ban-deprecated-sync-prop-setters](docs/rules/ban-deprecated-sync-prop-setters.md) | Ban use of deprecated synchronous property getters | 👍 🔦 | | 🔧 | +| [constrain-proportions-replaced-by-target-aspect-ratio-advice](docs/rules/constrain-proportions-replaced-by-target-aspect-ratio-advice.md) | Warns against using constrainProportions in favor of targetAspectRatio | | 👍 | | +| [dynamic-page-documentchange-event-advice](docs/rules/dynamic-page-documentchange-event-advice.md) | Advice on using the `documentchange` event | | 👍 | | +| [dynamic-page-find-method-advice](docs/rules/dynamic-page-find-method-advice.md) | Advice on using the find*() family of methods | | 👍 | | diff --git a/dist/index.js b/dist/index.js index 8aa4d1c..1688fca 100644 --- a/dist/index.js +++ b/dist/index.js @@ -8,6 +8,7 @@ const banDeprecatedSyncMethods_1 = require("./rules/banDeprecatedSyncMethods"); const banDeprecatedSyncPropGetters_1 = require("./rules/banDeprecatedSyncPropGetters"); const banDeprecatedSyncPropSetters_1 = require("./rules/banDeprecatedSyncPropSetters"); const dynamicPageFindMethodAdvice_1 = require("./rules/dynamicPageFindMethodAdvice"); +const constrainProportionsReplacedByTargetAspectRatioAdvice_1 = require("./rules/constrainProportionsReplacedByTargetAspectRatioAdvice"); function rulesetWithSeverity(severity, rules) { return Object.keys(rules).reduce((acc, name) => { acc[`@figma/figma-plugins/${name}`] = severity; @@ -25,15 +26,16 @@ const dynamicePageAdvice = { 'dynamic-page-documentchange-event-advice': dynamicPageDocumentchangeEventAdvice_1.dynamicPageDocumentchangeEventAdvice, 'dynamic-page-find-method-advice': dynamicPageFindMethodAdvice_1.dynamicPageFindMethodAdvice, }; +const warnRules = Object.assign(Object.assign({}, dynamicePageAdvice), { 'constrain-proportions-replaced-by-target-aspect-ratio-advice': constrainProportionsReplacedByTargetAspectRatioAdvice_1.constrainProportionsReplacedByTargetAspectRatioAdvice }); // The exported type annotations in this file are somewhat arbitrary; we do NOT // expect anyone to actually consume these types. We include them because we use // @figma as a type root, and all packages under a type root must emit a type // declaration file. -exports.rules = Object.assign(Object.assign({}, errRules), dynamicePageAdvice); +exports.rules = Object.assign(Object.assign({}, errRules), warnRules); exports.configs = { recommended: { plugins: ['@figma/figma-plugins'], - rules: Object.assign(Object.assign({}, rulesetWithSeverity('error', errRules)), rulesetWithSeverity('warn', dynamicePageAdvice)), + rules: Object.assign(Object.assign({}, rulesetWithSeverity('error', errRules)), rulesetWithSeverity('warn', warnRules)), }, 'recommended-problems-only': { plugins: ['@figma/figma-plugins'], diff --git a/dist/rules/constrainProportionsReplacedByTargetAspectRatioAdvice.d.ts b/dist/rules/constrainProportionsReplacedByTargetAspectRatioAdvice.d.ts new file mode 100644 index 0000000..95fdf13 --- /dev/null +++ b/dist/rules/constrainProportionsReplacedByTargetAspectRatioAdvice.d.ts @@ -0,0 +1,2 @@ +import type { TSESLint as _ } from '@typescript-eslint/utils'; +export declare const constrainProportionsReplacedByTargetAspectRatioAdvice: _.RuleModule<"readAdvice" | "writeAdvice", never[], _.RuleListener>; diff --git a/dist/rules/constrainProportionsReplacedByTargetAspectRatioAdvice.js b/dist/rules/constrainProportionsReplacedByTargetAspectRatioAdvice.js new file mode 100644 index 0000000..db17209 --- /dev/null +++ b/dist/rules/constrainProportionsReplacedByTargetAspectRatioAdvice.js @@ -0,0 +1,52 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.constrainProportionsReplacedByTargetAspectRatioAdvice = void 0; +const typescript_estree_1 = require("@typescript-eslint/typescript-estree"); +const util_1 = require("../util"); +exports.constrainProportionsReplacedByTargetAspectRatioAdvice = (0, util_1.createPluginRule)({ + name: 'constrain-proportions-replaced-by-target-aspect-ratio-advice', + meta: { + docs: { + description: 'Warns against using constrainProportions in favor of targetAspectRatio', + }, + messages: { + readAdvice: 'Please use targetAspectRatio instead of constrainProportions for determining if a node will resize proportinally.', + writeAdvice: 'Please use lockAspectRatio() or unlockAspectRatio() instead of setting constrainProportions.', + }, + schema: [], + type: 'suggestion', + }, + defaultOptions: [], + create(context) { + return { + MemberExpression(node) { + const property = node.property; + if (property.type === typescript_estree_1.AST_NODE_TYPES.Identifier && + property.name === 'constrainProportions') { + // Check if the receiver is a LayoutMixin, since that's what constrainProportions lives on + const match = (0, util_1.matchAncestorTypes)(context, node.object, ['LayoutMixin']); + if (!match) { + return; + } + // Check if it's being read or written to + const parent = node.parent; + if ((parent === null || parent === void 0 ? void 0 : parent.type) === typescript_estree_1.AST_NODE_TYPES.AssignmentExpression && + parent.left === node) { + // It's being written to + context.report({ + node, + messageId: 'writeAdvice', + }); + } + else { + // It's being read + context.report({ + node, + messageId: 'readAdvice', + }); + } + } + }, + }; + }, +}); diff --git a/docs/rules/constrain-proportions-replaced-by-target-aspect-ratio-advice.md b/docs/rules/constrain-proportions-replaced-by-target-aspect-ratio-advice.md new file mode 100644 index 0000000..baa3272 --- /dev/null +++ b/docs/rules/constrain-proportions-replaced-by-target-aspect-ratio-advice.md @@ -0,0 +1,5 @@ +# Warns against using constrainProportions in favor of targetAspectRatio (`@figma/figma-plugins/constrain-proportions-replaced-by-target-aspect-ratio-advice`) + +⚠️ This rule _warns_ in the 👍 `recommended` config. + + diff --git a/src/index.ts b/src/index.ts index bf268c6..96e09a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import { banDeprecatedSyncMethods } from './rules/banDeprecatedSyncMethods' import { banDeprecatedSyncPropGetters } from './rules/banDeprecatedSyncPropGetters' import { banDeprecatedSyncPropSetters } from './rules/banDeprecatedSyncPropSetters' import { dynamicPageFindMethodAdvice } from './rules/dynamicPageFindMethodAdvice' +import { constrainProportionsReplacedByTargetAspectRatioAdvice } from './rules/constrainProportionsReplacedByTargetAspectRatioAdvice' function rulesetWithSeverity( severity: 'error' | 'warn', @@ -29,6 +30,11 @@ const dynamicePageAdvice: Record = { 'dynamic-page-find-method-advice': dynamicPageFindMethodAdvice, } +const warnRules: Record = { + ...dynamicePageAdvice, + 'constrain-proportions-replaced-by-target-aspect-ratio-advice': constrainProportionsReplacedByTargetAspectRatioAdvice +} + // The exported type annotations in this file are somewhat arbitrary; we do NOT // expect anyone to actually consume these types. We include them because we use // @figma as a type root, and all packages under a type root must emit a type @@ -36,7 +42,7 @@ const dynamicePageAdvice: Record = { export const rules: unknown = { ...errRules, - ...dynamicePageAdvice, + ...warnRules } export const configs: unknown = { @@ -44,7 +50,7 @@ export const configs: unknown = { plugins: ['@figma/figma-plugins'], rules: { ...rulesetWithSeverity('error', errRules), - ...rulesetWithSeverity('warn', dynamicePageAdvice), + ...rulesetWithSeverity('warn', warnRules), }, }, 'recommended-problems-only': { diff --git a/src/rules/constrainProportionsReplacedByTargetAspectRatioAdvice.ts b/src/rules/constrainProportionsReplacedByTargetAspectRatioAdvice.ts new file mode 100644 index 0000000..a7a6871 --- /dev/null +++ b/src/rules/constrainProportionsReplacedByTargetAspectRatioAdvice.ts @@ -0,0 +1,59 @@ +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' +import { createPluginRule, matchAncestorTypes } from '../util' + +// Copied from dynamicPageFindMethodAdvice +// Calls to createPluginRule() cause typechecker errors without this import. +// Needed for TypeScript bug +import type { TSESLint as _ } from '@typescript-eslint/utils' + +export const constrainProportionsReplacedByTargetAspectRatioAdvice = createPluginRule({ + name: 'constrain-proportions-replaced-by-target-aspect-ratio-advice', + meta: { + docs: { + description: 'Warns against using constrainProportions in favor of targetAspectRatio', + }, + messages: { + readAdvice: 'Please use targetAspectRatio instead of constrainProportions for determining if a node will resize proportinally.', + writeAdvice: 'Please use lockAspectRatio() or unlockAspectRatio() instead of setting constrainProportions.', + }, + schema: [], + type: 'suggestion', + }, + defaultOptions: [], + create(context) { + return { + MemberExpression(node: TSESTree.MemberExpression) { + const property = node.property + if ( + property.type === AST_NODE_TYPES.Identifier && + property.name === 'constrainProportions' + ) { + // Check if the receiver is a LayoutMixin, since that's what constrainProportions lives on + const match = matchAncestorTypes(context, node.object, ['LayoutMixin']) + if (!match) { + return + } + + // Check if it's being read or written to + const parent = node.parent + if ( + parent?.type === AST_NODE_TYPES.AssignmentExpression && + parent.left === node + ) { + // It's being written to + context.report({ + node, + messageId: 'writeAdvice', + }) + } else { + // It's being read + context.report({ + node, + messageId: 'readAdvice', + }) + } + } + }, + } + }, +}) \ No newline at end of file diff --git a/test/constrainProportionsReplacedByTargetAspectRatio.test.ts b/test/constrainProportionsReplacedByTargetAspectRatio.test.ts new file mode 100644 index 0000000..8693eb4 --- /dev/null +++ b/test/constrainProportionsReplacedByTargetAspectRatio.test.ts @@ -0,0 +1,59 @@ +import { constrainProportionsReplacedByTargetAspectRatioAdvice } from '../src/rules/constrainProportionsReplacedByTargetAspectRatioAdvice' +import { ruleTester } from './testUtil' + +const types = ` +interface LayoutMixin { + constrainProportions: boolean +} + +interface DefaultFrameMixin extends LayoutMixin {} + +interface FrameNode extends DefaultFrameMixin {} + +interface SceneNode extends LayoutMixin {} +` + +ruleTester().run('constrain-proportions-replaced-by-target-aspect-ratio', constrainProportionsReplacedByTargetAspectRatioAdvice, { + valid: [ + { + code: ` +${types} +function func(node: FrameNode) { + node.someOtherProp = true +} +`, + }, + ], + invalid: [ + { + // Test write case + code: ` +${types} +function func(node: SceneNode) { + node.constrainProportions = true +} +`, + errors: [{ messageId: 'writeAdvice' }], + }, + { + // Test write case + code: ` + ${types} + function func(node: FrameNode) { + node.constrainProportions = false + } + `, + errors: [{ messageId: 'writeAdvice' }], + }, + { + // Test read case + code: ` +${types} +function func(node: SceneNode) { + const value = node.constrainProportions +} +`, + errors: [{ messageId: 'readAdvice' }], + }, + ], +}) \ No newline at end of file