Skip to content

Commit

Permalink
Add postfix convention for store naming rule (#37)
Browse files Browse the repository at this point in the history
* add postfix mode

* change docs

* add tests to cover postfix mode

* validate mode and move correctedStoreName to report function

* fix validation

* use shared settings to be able to share name convention across different rules

* modify tests according to the new implementation

* modify docs and recommended config

* modify error message

* extract utilities

* fix no get state rule

* refactor enforce naming rule
  • Loading branch information
ilyaryabchinski committed Sep 26, 2021
1 parent 21ab289 commit 01e7175
Show file tree
Hide file tree
Showing 42 changed files with 426 additions and 34 deletions.
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ module.exports = {
},
configs: {
recommended: require("./config/recommended"),
},
}
};
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
const {
extractImportedFromEffector,
} = require("../../utils/extract-imported-from-effector");
const { isStoreNameValid } = require("../../utils/is-store-name-valid");
const { validateStoreNameConvention } = require("../../utils/validate-store-name-convention");
const { getStoreNameConvention } = require("../../utils/get-store-name-convention");
const { getCorrectedStoreName } = require("../../utils/get-corrected-store-name");

module.exports = {
meta: {
type: "problem",
docs: {
description:
"Enforce $ as a prefix for any store created by Effector methods",
"Enforce $ as a prefix or postfix for any store created by Effector methods",
category: "Naming",
recommended: true,
},
messages: {
invalidName:
'Store "{{ storeName }}" should be named with prefix, rename it to "${{ storeName }}"',
renameStore: 'Rename "{{ storeName }}" to "${{ storeName }}"',
'Store "{{ storeName }}" should be named with {{ storeNameConvention }}, rename it to "{{ correctedStoreName }}"',
renameStore: 'Rename "{{ storeName }}" to "{{ correctedStoreName }}"',
},
schema: [],
},
create(context) {
const parserServices = context.parserServices;
const { parserServices } = context;

validateStoreNameConvention(context);

// TypeScript-way
if (parserServices.hasFullTypeInformation) {
return {
Expand All @@ -29,20 +36,24 @@ module.exports = {
const type = checker.getTypeAtLocation(originalNode.initializer);

const isEffectorStore =
type?.symbol?.escapedName === "Store" &&
type?.symbol?.parent?.escapedName?.includes("effector");
type?.symbol?.escapedName === "Store" &&
type?.symbol?.parent?.escapedName?.includes("effector");

if (!isEffectorStore) {
return;
}

const storeName = node.id.name;

if (storeName?.startsWith("$")) {
if (isStoreNameValid(storeName, context)) {
return;
}

reportStoreNameConventionViolation({ context, node, storeName });
reportStoreNameConventionViolation({
context,
node,
storeName
});
},
};
}
Expand All @@ -68,71 +79,75 @@ module.exports = {
}

const resultSavedInVariable =
node.parent.type === "VariableDeclarator";
node.parent.type === "VariableDeclarator";
if (!resultSavedInVariable) {
continue;
}

const storeName = node.parent.id.name;
if (storeName.startsWith("$")) {

if (isStoreNameValid(storeName, context)) {
continue;
}

reportStoreNameConventionViolation({
context,
node: node.parent,
storeName,
storeName
});
return;
}

// Store creation with .map
if (node.callee?.property?.name === "map") {
const objectIsEffectorStore =
node.callee?.object?.name?.startsWith?.("$");
if (!objectIsEffectorStore) {
const storeNameCreatedFromMap = node.callee?.object?.name;

if (!isStoreNameValid(storeNameCreatedFromMap, context)) {
return;
}

const resultSavedInVariable =
node.parent.type === "VariableDeclarator";
node.parent.type === "VariableDeclarator";
if (!resultSavedInVariable) {
return;
}

const storeName = node.parent.id.name;
if (storeName.startsWith("$")) {

if (isStoreNameValid(storeName, context)) {
return;
}


reportStoreNameConventionViolation({
context,
node: node.parent,
storeName,
storeName
});
return;
}

// Store creation in domain
const STORE_IN_DOMAIN_CREATION_METHODS = ["createStore", "store"];
if (
STORE_IN_DOMAIN_CREATION_METHODS.includes(node.callee?.property?.name)
STORE_IN_DOMAIN_CREATION_METHODS.includes(node.callee?.property?.name)
) {
const resultSavedInVariable =
node.parent.type === "VariableDeclarator";
node.parent.type === "VariableDeclarator";
if (!resultSavedInVariable) {
return;
}

const storeName = node.parent.id.name;
if (storeName.startsWith("$")) {

if (isStoreNameValid(storeName, context)) {
return;
}

reportStoreNameConventionViolation({
context,
node: node.parent,
storeName,
storeName
});
return;
}
Expand All @@ -142,20 +157,27 @@ module.exports = {
};

function reportStoreNameConventionViolation({ context, node, storeName }) {

const storeNameConvention = getStoreNameConvention(context);
const correctedStoreName = getCorrectedStoreName(storeName, context);

context.report({
node,
messageId: "invalidName",
data: {
storeName,
correctedStoreName,
storeNameConvention
},
suggest: [
{
messageId: "renameStore",
data: { storeName },
fix(fixer) {
return fixer.insertTextBeforeRange(node.range, "$");
return fixer.replaceTextRange(node.id.range, correctedStoreName);
},
},
],
});
}

Original file line number Diff line number Diff line change
@@ -1,11 +1,44 @@
# effector/enforce-store-naming-convention

Enforcing naming conventions helps keep the codebase consistent, and reduces overhead when thinking about how to name a variable with store. Your stores should be distingueshed by a prefix $. For example, `$name` is a store,`name` is not.
Enforcing naming conventions helps keep the codebase consistent, and reduces overhead when thinking about how to name a variable with store. Depending on the configuration your stores should be distinguished by a prefix or a postfix $. Enforces prefix convention by default.

## Prefix convention
When configured as:
```js
module.exports = {
rules: {
"effector/enforce-store-naming-convention": "error",
},
};
```
Prefix convention will be enforced:
```ts
// 👍 nice name
const $name = createStore(null);

// 👎 bad name
const name = createStrore(null);
```
## Postfix convention

When configured as:
```js
module.exports = {
rules: {
"effector/enforce-store-naming-convention": "error",
},
settings: {
effector: {
storeNameConvention: "postfix"
}
}
};
```
Postfix convention will be enforced:
```ts
// 👍 nice name
const name$ = createStore(null);

// 👎 bad name
const name = createStrore(null);
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const { RuleTester } = require("eslint");

const { readExample } = require("../../../utils/read-example");

const rule = require("../enforce-store-naming-convention");

const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
},
});

const readExampleForTheRule = (name) => readExample(__dirname, name);

ruleTester.run("effector/enforce-store-naming-convention-postfix.test", rule, {
valid: [
"correct-store-naming.js",
"correct-store-naming-from-other-package.js",
"correct-store-naming-in-domain.js",
"correct-examples-issue-23.js",
]
.map(readExampleForTheRule)
.map((code) => ({
code,
settings: {
effector: {
storeNameConvention: "postfix"
}
},
})),

invalid: [
// Errors
...[
"incorrect-createStore.js",
"incorrect-createStore-alias.js",
"incorrect-createStore-prefix.js",
"incorrect-restore.js",
"incorrect-restore-alias.js",
"incorrect-combine.js",
"incorrect-combine-alias.js",
"incorrect-map.js",
"incorrect-createStore-domain.js",
]
.map(readExampleForTheRule)
.map((code) => ({
code,
settings: {
effector: {
storeNameConvention: "postfix"
}
},
errors: [
{
messageId: "invalidName",
type: "VariableDeclarator",
},
],
})),
// Suggestions
{
code: `
import {createStore} from 'effector';
const store = createStore(null);
`,
errors: [
{
messageId: "invalidName",
suggestions: [
{
messageId: "renameStore",
data: { storeName: "store" },
output: `
import {createStore} from 'effector';
const $store = createStore(null);
`,
},
],
},
],
},
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const { RuleTester } =
require("@typescript-eslint/experimental-utils").ESLintUtils;
const { join } = require("path");

const { readExample } = require("../../../utils/read-example");

const rule = require("../enforce-store-naming-convention");

const ruleTester = new RuleTester({
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
project: "./tsconfig.json",
tsconfigRootDir: join(__dirname, "../.."),
},
});

const readExampleForTheRule = (name) => ({
code: readExample(__dirname, name),
filename: join(__dirname, "examples", name),
settings: {
effector: {
storeNameConvention: "postfix"
}
},
});

ruleTester.run("effector/enforce-store-naming-convention-postfix.ts.test", rule, {
valid: ["correct-store-naming.ts"].map(readExampleForTheRule),

invalid: [
// Errors
...["incorrect-store-naming.ts"]
.map(readExampleForTheRule)
.map((result) => ({
...result,
errors: [
{
messageId: "invalidName",
type: "VariableDeclarator",
},
],
})),
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createDomain } from "effector";

const domain = createDomain();

const storeOne$ = domain.createStore(null);
const storeTwo$ = domain.store(null);

export { storeOne$, storeTwo$ };
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createStore, restore, createEvent, combine } from "effector";

// Just createStore
const justStore$ = createStore(null);

// Restore
const eventForRestore = createEvent();
const restoredStore$ = restore(eventForRestore, null);

// Combine
const combinedStore$ = combine($justStore, $restoredStore);

// Map
const mappedStore$ = $combinedStore.map((values) => values.length);

export { justStore$, restoredStore$, combinedStore$, mappedStore$ };

0 comments on commit 01e7175

Please sign in to comment.