Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor to reduce code duplication and refine types #116

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 3 additions & 6 deletions lib/rules/hierarchy-separator.ts
Expand Up @@ -3,9 +3,8 @@
* @author Yann Braga
*/

import { TSESTree } from '@typescript-eslint/utils'
import { getMetaObjectExpression } from '../utils'
import { isLiteral, isSpreadElement } from '../utils/ast'
import { getMetaObjectExpression, getObjectBareProperty } from '../utils'
import { isLiteral } from '../utils/ast'
import { CategoryId } from '../utils/constants'
import { createStorybookRule } from '../utils/create-storybook-rule'

Expand Down Expand Up @@ -40,9 +39,7 @@ export = createStorybookRule({
return null
}

const titleNode = meta.properties.find(
(prop) => !isSpreadElement(prop) && 'name' in prop.key && prop.key?.name === 'title'
) as TSESTree.MethodDefinition | TSESTree.Property | undefined
const titleNode = getObjectBareProperty(meta.properties, 'title')

if (!titleNode || !isLiteral(titleNode.value)) {
return
Expand Down
44 changes: 14 additions & 30 deletions lib/rules/no-redundant-story-name.ts
Expand Up @@ -4,16 +4,17 @@
*/

import { storyNameFromExport } from '@storybook/csf'
import {
getExportNamedIdentifierDeclaration,
getObjectBareProperty,
getObjectBarePropertyValue,
} from '../utils'

import {
isExpressionStatement,
isLiteral,
isIdentifier,
isObjectExpression,
isProperty,
isVariableDeclaration,
isMetaProperty,
isSpreadElement,
} from '../utils/ast'
import { CategoryId } from '../utils/constants'
import { createStorybookRule } from '../utils/create-storybook-rule'
Expand Down Expand Up @@ -50,34 +51,17 @@ export = createStorybookRule({
return {
// CSF3
ExportNamedDeclaration: function (node) {
// if there are specifiers, node.declaration should be null
if (!node.declaration) return

const decl = node.declaration
if (isVariableDeclaration(decl)) {
const declaration = decl.declarations[0]
if (declaration == null) return
const { id, init } = declaration
if (isIdentifier(id) && isObjectExpression(init)) {
const storyNameNode = init.properties.find(
(prop) =>
isProperty(prop) &&
isIdentifier(prop.key) &&
(prop.key?.name === 'name' || prop.key?.name === 'storyName')
)

if (!storyNameNode) {
return
}
const declaration = getExportNamedIdentifierDeclaration(node)
if (declaration && isObjectExpression(declaration.init)) {
const storyNameNode =
getObjectBareProperty(declaration.init.properties, 'name') ||
getObjectBareProperty(declaration.init.properties, 'storyName')

const { name } = id
const resolvedStoryName = storyNameFromExport(name)
if (storyNameNode) {
const resolvedStoryName = storyNameFromExport(declaration.id.name)
const storyName = getObjectBarePropertyValue(storyNameNode)

if (
!isSpreadElement(storyNameNode) &&
isLiteral(storyNameNode.value) &&
storyNameNode.value.value === resolvedStoryName
) {
if (storyName === resolvedStoryName) {
context.report({
node: storyNameNode,
messageId: 'storyNameIsRedundant',
Expand Down
7 changes: 2 additions & 5 deletions lib/rules/no-title-property-in-meta.ts
Expand Up @@ -4,10 +4,9 @@
*/

import { TSESTree } from '@typescript-eslint/utils'
import { getMetaObjectExpression } from '../utils'
import { getMetaObjectExpression, getObjectBareProperty } from '../utils'
import { CategoryId } from '../utils/constants'
import { createStorybookRule } from '../utils/create-storybook-rule'
import { isSpreadElement } from '../utils/ast'

//------------------------------------------------------------------------------
// Rule Definition
Expand Down Expand Up @@ -39,9 +38,7 @@ export = createStorybookRule({
return null
}

const titleNode = meta.properties.find(
(prop) => !isSpreadElement(prop) && 'name' in prop.key && prop.key?.name === 'title'
)
const titleNode = getObjectBareProperty(meta.properties, 'title')

if (titleNode) {
context.report({
Expand Down
62 changes: 23 additions & 39 deletions lib/rules/no-uninstalled-addons.ts
Expand Up @@ -9,16 +9,10 @@ import { resolve, relative, sep } from 'path'

import { createStorybookRule } from '../utils/create-storybook-rule'
import { CategoryId } from '../utils/constants'
import {
isObjectExpression,
isProperty,
isIdentifier,
isArrayExpression,
isLiteral,
isVariableDeclarator,
isVariableDeclaration,
} from '../utils/ast'
import { isObjectExpression, isArrayExpression, isLiteral } from '../utils/ast'
import { TSESTree } from '@typescript-eslint/utils'
import { getExportNamedIdentifierDeclaration, getObjectBareProperty } from '../utils'
import { Maybe, NamedVariable, ObjectLiteralItem } from '../types'

//------------------------------------------------------------------------------
// Rule Definition
Expand Down Expand Up @@ -152,10 +146,8 @@ export = createStorybookRule({
const nodesWithAddonsInObj = addonsExpression.elements
.map((elem) => (isObjectExpression(elem) ? elem : { properties: [] }))
.map((elem) => {
const property: TSESTree.Property = elem.properties.find(
(prop) => isProperty(prop) && isIdentifier(prop.key) && prop.key.name === 'name'
) as TSESTree.Property
return isLiteral(property?.value)
const property = getObjectBareProperty(elem.properties, 'name')
return property && isLiteral(property?.value)
? { value: property.value.value, node: property.value }
: undefined
})
Expand Down Expand Up @@ -208,46 +200,38 @@ export = createStorybookRule({
}
}

function checkAddonInstall<
AddonsProp extends ObjectLiteralItem | NamedVariable,
Property = AddonsProp extends ObjectLiteralItem ? 'value' : 'init'
>(addonsProp: AddonsProp, prop: Property extends keyof AddonsProp ? Property : never) {
if (addonsProp && addonsProp[prop]) {
const node = addonsProp[prop] as Maybe<TSESTree.Node>
if (isArrayExpression(node)) {
reportUninstalledAddons(node)
}
}
}

//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------

return {
AssignmentExpression: function (node) {
if (isObjectExpression(node.right)) {
const addonsProp = node.right.properties.find(
(prop): prop is TSESTree.Property =>
isProperty(prop) && isIdentifier(prop.key) && prop.key.name === 'addons'
)

if (addonsProp && addonsProp.value && isArrayExpression(addonsProp.value)) {
reportUninstalledAddons(addonsProp.value)
}
const addonsProp = getObjectBareProperty(node.right.properties, 'addons')
if (addonsProp) checkAddonInstall(addonsProp, 'value')
}
},
ExportDefaultDeclaration: function (node) {
if (isObjectExpression(node.declaration)) {
const addonsProp = node.declaration.properties.find(
(prop): prop is TSESTree.Property =>
isProperty(prop) && isIdentifier(prop.key) && prop.key.name === 'addons'
)

if (addonsProp && addonsProp.value && isArrayExpression(addonsProp.value)) {
reportUninstalledAddons(addonsProp.value)
}
const addonsProp = getObjectBareProperty(node.declaration.properties, 'addons')
if (addonsProp) checkAddonInstall(addonsProp, 'value')
}
},
ExportNamedDeclaration: function (node) {
const addonsProp =
isVariableDeclaration(node.declaration) &&
node.declaration.declarations.find(
(decl) =>
isVariableDeclarator(decl) && isIdentifier(decl.id) && decl.id.name === 'addons'
)

if (addonsProp && isArrayExpression(addonsProp.init)) {
reportUninstalledAddons(addonsProp.init)
}
const addonsProp = getExportNamedIdentifierDeclaration(node, 'addons')
if (addonsProp) checkAddonInstall(addonsProp, 'init')
},
}
},
Expand Down
21 changes: 8 additions & 13 deletions lib/rules/prefer-pascal-case.ts
Expand Up @@ -6,8 +6,11 @@
import { ASTUtils, TSESTree } from '@typescript-eslint/utils'
import { IncludeExcludeOptions, isExportStory } from '@storybook/csf'

import { getDescriptor, getMetaObjectExpression } from '../utils'
import { isIdentifier, isVariableDeclaration } from '../utils/ast'
import {
getDescriptor,
getExportNamedIdentifierDeclarations,
getMetaObjectExpression,
} from '../utils'
import { CategoryId } from '../utils/constants'
import { createStorybookRule } from '../utils/create-storybook-rule'

Expand Down Expand Up @@ -102,7 +105,7 @@ export = createStorybookRule({

let meta
let nonStoryExportsConfig: IncludeExcludeOptions
const namedExports: TSESTree.Identifier[] = []
let namedExports: TSESTree.Identifier[] = []
let hasStoriesOfImport = false

return {
Expand All @@ -127,16 +130,8 @@ export = createStorybookRule({
ExportNamedDeclaration: function (node: TSESTree.ExportNamedDeclaration) {
// if there are specifiers, node.declaration should be null
if (!node.declaration) return

const decl = node.declaration
if (isVariableDeclaration(decl)) {
const declaration = decl.declarations[0]
if (declaration == null) return
const { id } = declaration
if (isIdentifier(id)) {
namedExports.push(id)
}
}
const declarations = (getExportNamedIdentifierDeclarations(node) ?? []).map(({ id }) => id)
namedExports = [...namedExports, ...declarations]
},
'Program:exit': function () {
if (namedExports.length && !hasStoriesOfImport) {
Expand Down
10 changes: 9 additions & 1 deletion lib/types/index.ts
@@ -1,4 +1,4 @@
import { TSESLint } from '@typescript-eslint/utils'
import { TSESLint, TSESTree } from '@typescript-eslint/utils'
import { CategoryId } from '../utils/constants'

export type RuleModule = TSESLint.RuleModule<'', []> & {
Expand Down Expand Up @@ -39,3 +39,11 @@ export type StorybookRuleMeta<TMessageIds extends string> = Omit<
// schema: [],
// docs,
// }

export type NamedVariable = TSESTree.VariableDeclarator & { id: TSESTree.Identifier }

export type ObjectLiteralItem = Exclude<TSESTree.ObjectLiteralElement, TSESTree.SpreadElement>

export type StoryDescriptor = string[] | RegExp

export type Maybe<T> = T | null | undefined
3 changes: 2 additions & 1 deletion lib/utils/ast.ts
@@ -1,9 +1,10 @@
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'
import { Maybe } from '../types'
export { ASTUtils } from '@typescript-eslint/utils'

const isNodeOfType =
<NodeType extends AST_NODE_TYPES>(nodeType: NodeType) =>
(node: TSESTree.Node | null | undefined): node is TSESTree.Node & { type: NodeType } =>
(node: Maybe<TSESTree.Node>): node is TSESTree.Node & { type: NodeType } =>
node?.type === nodeType

export const isAwaitExpression = isNodeOfType(AST_NODE_TYPES.AwaitExpression)
Expand Down