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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add rule no inline styles #224

Merged
merged 7 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export default defineConfig({
{ text: 'If Without Curly Braces', link: '/rules/rrd/if-without-curly-braces' },
{ text: 'Magic Numbers', link: '/rules/rrd/magic-numbers' },
{ text: 'Nested Ternary', link: '/rules/rrd/nested-ternary' },
{ text: 'No Inline Styles', link: '/rules/rrd/no-inline-styles' },
{ text: 'No Props Destructure', link: '/rules/rrd/no-prop-destructure' },
{ text: 'No Var Declaration', link: '/rules/rrd/no-var-declaration' },
{ text: 'Parameter Count', link: '/rules/rrd/parameter-count' },
Expand Down
1 change: 1 addition & 0 deletions docs/rules/rrd/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ These ruleset is the most opinionated with rules that are not part of the _offic
- [If Without Curly Braces](./if-without-curly-braces.md)
- [Magic Numbers](./magic-numbers.md)
- [Nested Ternary](./nested-ternary.md)
- [No Inline Styles](./no-inline-styles.md)
- [No Prop Destructing](./no-prop-destructure.md)
- [No Var Declaration](./no-var-declaration.md)
- [Parameter Count](./parameter-count.md)
Expand Down
43 changes: 43 additions & 0 deletions docs/rules/rrd/no-inline-styles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# No Inline Styles

Checks if in an element within the template of an SFC uses a inline style

## ❓ Why it's good to follow this rule?

**Maintainability:** External CSS makes updates easier and keeps the codebase clean.
**Performance:** Reduces HTML file size and leverages browser caching.
**Consistency:** Ensures a uniform style across the entire website or application.

## 😱 Examples of code for which this rule will throw a warning

::: warning
The following code contains inline `styles` which are not recommended because they increase the html code and do not take advantage of browser caching
:::

```vue
<template>
<div style="background-color: #ff0000;">
<!-- ... -->
</div>
</template>
```

## 🤩 How to fix it?

::: tip
Using `style` with the `scoped` attribute and creating the necessary classes for the style you want to apply can be a good option.
:::

```vue
<template>
<div class="bg-primary">
<!-- ... -->
</div>
</template>

<style scoped>
.bg-primary {
background-color: #ff0000;
}
</style>
```
1 change: 1 addition & 0 deletions src/rules/rrd/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export * from './shortVariableName'
export * from './tooManyProps'
export * from './vForWithIndexKey'
export * from './zeroLengthComparison'
export * from './noInlineStyles'
57 changes: 57 additions & 0 deletions src/rules/rrd/noInlineStyles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { beforeEach, describe, expect, it } from 'vitest'
import type { SFCTemplateBlock } from '@vue/compiler-sfc'
import { BG_RESET, BG_WARN, TEXT_INFO, TEXT_RESET, TEXT_WARN } from '../asceeCodes'
import { checkNoInlineStyles, reportNoInlineStyles, resetNoInlineStyles } from './noInlineStyles'

describe('checkNoInlineStyles', () => {
beforeEach(() => {
resetNoInlineStyles()
})

it('should not report files with inline styles', () => {
const template = {
content: `
<template>
<div>Hello World!</div>
<div>
<span>Hi!</span>
</div>
</template>
`,
} as SFCTemplateBlock
const fileName = 'noInlineStyles.vue'
checkNoInlineStyles(template, fileName)
expect(reportNoInlineStyles().length).toBe(0)
expect(reportNoInlineStyles()).toStrictEqual([])
})

it('should report files with inline styles', () => {
const template = {
content: `
<template>
<div style="background: #fff;">Hello World!</div>
<div>
<span style="color: red;">Hi!</span>
</div>
</template>
`,
} as SFCTemplateBlock
const fileName = 'noInlineStyles-problem.vue'
checkNoInlineStyles(template, fileName)
expect(reportNoInlineStyles().length).toBe(2)
expect(reportNoInlineStyles()).toStrictEqual([
{
file: fileName,
rule: `${TEXT_INFO}rrd ~ no Inline Styles${TEXT_RESET}`,
description: `👉 ${TEXT_WARN}Avoid using inline styles. Consider moving the styles to a CSS or SCSS file, or use a Vue scoped style.${TEXT_RESET}`,
message: `line #2 ${BG_WARN}Found inline style: style="background: #fff;"${BG_RESET} 🚨`,
},
{
file: fileName,
rule: `${TEXT_INFO}rrd ~ no Inline Styles${TEXT_RESET}`,
description: `👉 ${TEXT_WARN}Avoid using inline styles. Consider moving the styles to a CSS or SCSS file, or use a Vue scoped style.${TEXT_RESET}`,
message: `line #4 ${BG_WARN}Found inline style: style="color: red;"${BG_RESET} 🚨`,
},
])
})
})
44 changes: 44 additions & 0 deletions src/rules/rrd/noInlineStyles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { SFCTemplateBlock } from '@vue/compiler-sfc'
import getLineNumber from '../getLineNumber'
import type { FileCheckResult, Offense } from '../../types'
import { BG_RESET, BG_WARN, TEXT_INFO, TEXT_RESET, TEXT_WARN } from '../asceeCodes'

const results: FileCheckResult[] = []

const checkNoInlineStyles = (template: SFCTemplateBlock | null, filePath: string) => {
if (!template) {
return
}

const regex = /style\s*=\s*['"][^'"]*['"]/g

const matches = [...template.content.matchAll(regex)]

matches?.forEach((match) => {
const lineNumber = getLineNumber(template.content.trim(), match[0])
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add the from parameter here otherwise if the same pattern found multiple times, it will report the same line number for them

results.push({
filePath,
message: `line #${lineNumber} ${BG_WARN}Found inline style: ${match[0]}${BG_RESET}`,
})
})
}

const reportNoInlineStyles = () => {
const offenses: Offense[] = []

if (results.length > 0) {
results.forEach((result) => {
offenses.push({
file: result.filePath,
rule: `${TEXT_INFO}rrd ~ no Inline Styles${TEXT_RESET}`,
description: `👉 ${TEXT_WARN}Avoid using inline styles. Consider moving the styles to a CSS or SCSS file, or use a Vue scoped style.${TEXT_RESET}`,
message: `${result.message} 🚨`,
})
})
}
return offenses
}

const resetNoInlineStyles = () => (results.length = 0)

export { checkNoInlineStyles, reportNoInlineStyles, resetNoInlineStyles }
1 change: 1 addition & 0 deletions src/rules/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const RULES = {
'ifWithoutCurlyBraces',
'magicNumbers',
'nestedTernary',
'noInlineStyles',
'noPropDestructure',
'noVarDeclaration',
'parameterCount',
Expand Down
3 changes: 2 additions & 1 deletion src/rulesCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { checkElementSelectorsWithScoped, checkImplicitParentChildCommunication
import { checkGlobalStyle, checkSimpleProp, checkSingleNameComponent, checkVforNoKey, checkVifWithVfor } from './rules/vue-essential'
import { checkElementAttributeOrder, checkTopLevelElementOrder } from './rules/vue-recommended'
import { checkComponentFilenameCasing, checkComponentFiles, checkDirectiveShorthands, checkFullWordComponentName, checkMultiAttributeElements, checkPropNameCasing, checkQuotedAttributeValues, checkSelfClosingComponents, checkSimpleComputed, checkTemplateSimpleExpression } from './rules/vue-strong'
import { checkBigVif, checkBigVshow, checkComplicatedConditions, checkComputedSideEffects, checkCyclomaticComplexity, checkDeepIndentation, checkElseCondition, checkFunctionSize, checkHtmlImageElements, checkHtmlLink, checkIfWithoutCurlyBraces, checkMagicNumbers, checkNestedTernary, checkNoPropDestructure, checkNoVarDeclaration, checkParameterCount, checkPlainScript, checkPropsDrilling, checkScriptLength, checkShortVariableName, checkTooManyProps, checkVForWithIndexKey, checkZeroLengthComparison } from './rules/rrd'
import { checkBigVif, checkBigVshow, checkComplicatedConditions, checkComputedSideEffects, checkCyclomaticComplexity, checkDeepIndentation, checkElseCondition, checkFunctionSize, checkHtmlImageElements, checkHtmlLink, checkIfWithoutCurlyBraces, checkMagicNumbers, checkNestedTernary, checkNoInlineStyles, checkNoPropDestructure, checkNoVarDeclaration, checkParameterCount, checkPlainScript, checkPropsDrilling, checkScriptLength, checkShortVariableName, checkTooManyProps, checkVForWithIndexKey, checkZeroLengthComparison } from './rules/rrd'
import { getIsNuxt } from './context'

export const checkRules = (descriptor: SFCDescriptor, filePath: string, apply: string[]) => {
Expand Down Expand Up @@ -67,6 +67,7 @@ export const checkRules = (descriptor: SFCDescriptor, filePath: string, apply: s
tooManyProps: () => checkTooManyProps(script, filePath),
vForWithIndexKey: () => isVueFile && checkVForWithIndexKey(descriptor.template, filePath),
zeroLengthComparison: () => checkZeroLengthComparison(script, filePath),
noInlineStyles: () => checkNoInlineStyles(descriptor.template, filePath),
}

// Run the checks for each applied rule or ruleset
Expand Down
3 changes: 2 additions & 1 deletion src/rulesReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { reportElementSelectorsWithScoped, reportImplicitParentChildCommunicatio
import { reportGlobalStyle, reportSimpleProp, reportSingleNameComponent, reportVforNoKey, reportVifWithVfor } from './rules/vue-essential'
import { reportElementAttributeOrder, reportTopLevelElementOrder } from './rules/vue-recommended'
import { reportComponentFilenameCasing, reportComponentFiles, reportDirectiveShorthands, reportFullWordComponentName, reportMultiAttributeElements, reportPropNameCasing, reportQuotedAttributeValues, reportSelfClosingComponents, reportSimpleComputed, reportTemplateSimpleExpression } from './rules/vue-strong'
import { reportBigVif, reportBigVshow, reportComplicatedConditions, reportComputedSideEffects, reportCyclomaticComplexity, reportDeepIndentation, reportElseCondition, reportFunctionSize, reportHtmlImageElements, reportHtmlLink, reportIfWithoutCurlyBraces, reportMagicNumbers, reportNestedTernary, reportNoPropDestructure, reportNoVarDeclaration, reportParameterCount, reportPlainScript, reportPropsDrilling, reportScriptLength, reportShortVariableName, reportTooManyProps, reportVForWithIndexKey, reportZeroLengthComparison } from './rules/rrd'
import { reportBigVif, reportBigVshow, reportComplicatedConditions, reportComputedSideEffects, reportCyclomaticComplexity, reportDeepIndentation, reportElseCondition, reportFunctionSize, reportHtmlImageElements, reportHtmlLink, reportIfWithoutCurlyBraces, reportMagicNumbers, reportNestedTernary, reportNoInlineStyles, reportNoPropDestructure, reportNoVarDeclaration, reportParameterCount, reportPlainScript, reportPropsDrilling, reportScriptLength, reportShortVariableName, reportTooManyProps, reportVForWithIndexKey, reportZeroLengthComparison } from './rules/rrd'

// TODO move out to types
interface OutputType {
Expand Down Expand Up @@ -85,6 +85,7 @@ export const reportRules = (groupBy: GroupBy, sortBy: SortBy, level: OutputLevel
processOffenses(reportTooManyProps)
processOffenses(reportVForWithIndexKey)
processOffenses(reportZeroLengthComparison)
processOffenses(reportNoInlineStyles)

// Sort offenses grouped by key based on the `sortBy` parameter
const sortedKeys = Object.keys(offensesGrouped).sort((a, b) => {
Expand Down
Loading