Skip to content

Commit

Permalink
Merge pull request #1359 from IBM/527-sass-linting
Browse files Browse the repository at this point in the history
feat(linting): Improve sass linting using stylelint and a custom formatter
  • Loading branch information
tay1orjones committed Jul 14, 2020
2 parents 438ad28 + 82bfd2c commit 614112f
Show file tree
Hide file tree
Showing 14 changed files with 276 additions and 22 deletions.
123 changes: 123 additions & 0 deletions config/stylelint/sassMsgFormatter.js
@@ -0,0 +1,123 @@
const chalk = require('chalk');

/**
* @constant
*/
const ERROR = chalk.bold.red;
const WARNING = chalk.yellow;
const URL = chalk.underline.cyan;
const NUMBER = chalk.dim;
const RULE = chalk.bgWhite.black;

/**
* returns the boolean representing whether a file has any errors for filtering purposes
* @param {Object} result - the object representing the result of a linting session
* @returns {Boolean} returns true or false depending on whether the file has errors or not
*/
function filterForErrors(result) {
return result.errored;
}

/**
* creates colored text that describes the severity of a problem
* @param {String} severity - error.severity, the severity of the error
* @returns {String} returns colored error text depending on the severity
*/
function generateErrorIcon(severity) {
let errorIcon = '';
switch (severity) {
case 'error':
errorIcon = ERROR('ERROR');
break;
case 'warning':
errorIcon = WARNING('warning');
break;
default:
errorIcon += '';
break;
}
return errorIcon;
}

/**
* indents a piece of text depending on the length of the previous block of text
* @param {Object} error - the specific error
* @returns {String} returns the appropriate amount of tab characters
*/
function formatTabbing(error) {
if (`${error.line}:${error.column}`.length < 6) {
return '\t \t';
}
return '\t';
}

/**
* creates a message with a link to the carbon documentation depending on the individual error
* @param {String} text - the error.text property of an error message, the defaul error message stylelint produces
* @returns {String} returns a custom informational message for the type of error
*/
function createCustomMessage(text) {
let message = '';
if (text) {
if (text.includes('color"')) {
const url = URL('https://www.carbondesignsystem.com/guidelines/color/usage');
message = `\n\t${text}\n\t> Please refer to the Carbon documentation for proper color tokens: ${url}`;
} else if (text.includes('"margin') || text.includes('"padding')) {
const url = URL('https://www.carbondesignsystem.com/guidelines/spacing#spacing-scale');
message = `\n\t${text}\n\t> Please refer to the Carbon documentation for proper spacing values: ${url}`;
} else if (text.includes('"font')) {
const url = URL('https://www.carbondesignsystem.com/guidelines/typography/productive');
message = `\n\t${text}\n\t> Please refer to the Carbon productive typography documentation for proper font values: ${url}`;
} else if (text.includes('transition')) {
const url = URL('https://www.carbondesignsystem.com/guidelines/motion/overview');
message = `\n\t${text}\n\t> Please refer to the Carbon motion documentation for transitions: ${url}`;
}
}
return message;
}

/**
* formats the error message
* @param {Array<object>} errors - an array of all the errors in the linting session
* @returns {String} returns a formatted error message
*/

function formatError(errors) {
let errorMsg = '';
errors.forEach((error, i) => {
const number = NUMBER(`${i + 1}.`);
errorMsg += `${number} \t${generateErrorIcon(error.severity)} \t ${error.line}:${
error.column
} ${formatTabbing(error)} ${RULE(error.rule)}\n \t${createCustomMessage(error.text)}\n\n`;
});

return errorMsg;
}

/**
* @type {import('stylelint').Formatter}
*/
function formatter(results) {
let formattedMsg = '';
if (results) {
const filesWithErrors = results.filter(filterForErrors);
if (filesWithErrors.length > 0) {
formattedMsg += TITLE('\n!! WARNINGS !!\n\n');
}
filesWithErrors.forEach(result => {
const errors = result.warnings;
const errorMessage = formatError(errors);
formattedMsg += chalk.bold('Source: ');
formattedMsg += `${result.source}\n`;
formattedMsg += `${errorMessage}\n`;
});
}
return formattedMsg;
}

const formatterModule = (module.exports = formatter);
formatterModule.filterForErrors = filterForErrors;
formatterModule.generateErrorIcon = generateErrorIcon;
formatterModule.formatTabbing = formatTabbing;
formatterModule.createCustomMessage = createCustomMessage;
formatterModule.formatError = formatError;
89 changes: 89 additions & 0 deletions config/stylelint/sassMsgFormatter.test.js
@@ -0,0 +1,89 @@
const formatter = require('./sassMsgFormatter');
const chalk = require('chalk');

const ERROR = chalk.bold.red;
const WARNING = chalk.yellow;
const URL = chalk.underline.cyan;
const TITLE = chalk.bgYellow;

const exampleOne = {
source: 'test1.js',
errored: true,
warnings: [
{
severity: 'error',
line: 21,
column: 33,
rule: 'declaration-property-unit-blacklist',
text: 'Unexpected value in property "color"',
},
],
};
const exampleTwo = {
source: 'test2.js',
errored: true,
warnings: [
{
severity: 'error',
line: 123,
column: 76,
rule: 'declaration-property-value-blacklist',
text: 'Unexpected value in "font-family"',
},
],
};

describe('sassMsgFormatter', () => {
it('filters for errors', () => {
// this function should return false unless errored is equal to true
expect(formatter.filterForErrors({ errored: undefined })).toBeFalsy();
expect(formatter.filterForErrors({ errored: true })).toBeTruthy();
expect(formatter.filterForErrors({ errored: false })).toBeFalsy();
expect(formatter.filterForErrors({ errored: null })).toBeFalsy();
});
it('generates error icons', () => {
// expect(generateErrorIcon('error')).toEqual(ERROR('ERROR'));
expect(formatter.generateErrorIcon('error')).toContain('ERROR');
expect(formatter.generateErrorIcon('warning')).toContain('warning');
expect(formatter.generateErrorIcon(undefined)).toHaveLength(0);
});
it('formats tabbing', () => {
expect(formatter.formatTabbing({ line: 121, column: 23 })).toHaveLength(1);
expect(formatter.formatTabbing({ line: 1, column: 123 })).toHaveLength(3);
expect(formatter.formatTabbing({ line: 14, column: 23 })).toHaveLength(3);
});
it('creates custom message', () => {
console.log(exampleOne.warnings[0].text);
expect(createCustomMessage(exampleOne.warnings[0].text)).toContain(
URL('https://www.carbondesignsystem.com/guidelines/color/usage')
);
expect(createCustomMessage(exampleTwo.warnings[0].text)).toContain(
URL('https://www.carbondesignsystem.com/guidelines/typography/productive')
);
expect(createCustomMessage(null)).toHaveLength(0);
});
it('formats errors', () => {
expect(formatter.formatError(exampleOne.warnings)).toContain(ERROR('ERROR'));
expect(formatter.formatError(exampleTwo.warnings)).toContain(ERROR('ERROR'));
expect(
formatError([
{
severity: 'warning',
line: 123,
column: 45,
rule: 'insert-rule-here',
text: 'error text',
},
])
).toContain(WARNING('warning'));
});
it('formats message', () => {
const resultsTest = [exampleOne, exampleTwo, { errored: false, warnings: undefined }];
expect(resultsTest.filter(formatter.filterForErrors)).toHaveLength(2);
expect(formatter(resultsTest)).toContain(TITLE('\n!! WARNINGS !!\n\n'));
expect(formatter(resultsTest)).toContain(formatError(exampleOne.warnings));
expect(formatter(resultsTest)).toContain(formatError(exampleTwo.warnings));
expect(formatter([{ errored: false, warnings: undefined }])).toMatch('');
expect(formatter(null)).toHaveLength(0);
});
});
43 changes: 41 additions & 2 deletions package.json
Expand Up @@ -31,7 +31,8 @@
"format:diff": "prettier --list-different \"**/*.{scss,css,js,jsx,md,ts}\"",
"lint": "yarn lint:javascript && yarn lint:stylelint",
"lint:javascript": "eslint --ext .jsx --ext .js .",
"lint:stylelint": "stylelint './src/**/*.scss' --syntax scss --ignorePath .gitignore",
"lint:stylelint": "stylelint './src/**/*.scss' --syntax scss --ignorePath .gitignore --custom-formatter ./config/stylelint/sassMsgFormatter.js",
"prepare": "yarn build",
"publish-npm": "yarn semantic-release",
"start": "yarn test:engines && yarn storybook",
"storybook": "yarn test:engines && start-storybook -p 3000 -s public/development",
Expand All @@ -47,7 +48,44 @@
"extends": "stylelint-config-recommended-scss",
"plugins": [
"stylelint-scss"
]
],
"rules": {
"declaration-property-unit-blacklist": [
{
"font-size": [
"em",
"px",
"pt"
],
"margin": [
"px",
"rem"
],
"padding": [
"px",
"rem"
],
"transition": [
"s",
"ms"
]
},
{
"severity": "error"
}
],
"declaration-property-value-blacklist": [
{
"color": [
"/^#/",
"/^rgb/"
]
},
{
"severity": "error"
}
]
}
},
"husky": {
"hooks": {
Expand Down Expand Up @@ -162,6 +200,7 @@
"babel-plugin-require-context-hook": "^1.0.0",
"babel-plugin-styled-components": "^1.10.7",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"chalk": "^4.1.0",
"check-node-version": "^4.0.3",
"coveralls": "^3.0.2",
"cross-env": "^6.0.3",
Expand Down
2 changes: 1 addition & 1 deletion src/components/GaugeCard/_gauge-card.scss
Expand Up @@ -32,7 +32,7 @@
stroke-dasharray: 0 var(--stroke-dash-array);
transform: rotate(-90deg);
transform-origin: center;
transition: all 0.2s ease-in;
transition: all $duration--moderate-01 ease-in;

.#{$iot-prefix}--gauge__loaded & {
stroke-dasharray: var(--stroke-dash) var(--stroke-dash-array);
Expand Down
12 changes: 5 additions & 7 deletions src/components/Header/_header.scss
Expand Up @@ -59,7 +59,7 @@
.#{$prefix}--header__menu .#{$prefix}--header__menu-item[role='menuitem'] {
display: flex;
align-items: center;
color: #c6c6c6;
color: $active-ui;
height: 100%;
font-size: 0.875rem;
font-weight: 400;
Expand All @@ -68,7 +68,8 @@
text-decoration: none;
user-select: none;
border: 2px solid #0000;
transition: background-color 110ms, border-color 110ms, color 110ms;
transition: background-color $duration--fast-02, border-color $duration--fast-02,
color $duration--fast-02;
justify-content: center;
padding: 0;
width: 100%;
Expand Down Expand Up @@ -100,10 +101,7 @@
// Used for links that are directly in the menubar to span the full height
height: 100%;
// Text styles
font-size: rem(14px);
font-weight: 400;
letter-spacing: 0;
line-height: rem(18px);
@include carbon--type-style('body-short-01');
// Reset link styles and make sure the text isn't selectable
text-decoration: none;
user-select: none;
Expand All @@ -119,7 +117,7 @@ button.#{$prefix}--header__menu-item {

&:focus {
border-color: #fff;
color: #f4f4f4;
color: $ui-02;
outline: none;
}
}
2 changes: 1 addition & 1 deletion src/components/SideNav/SideNav.story.scss
Expand Up @@ -3,7 +3,7 @@
.#{$iot-prefix}--main-content {
padding-top: 3rem;
height: 100%;
transition: all 0.11s cubic-bezier(0.2, 0, 1, 0.9);
transition: all $duration--fast-02 cubic-bezier(0.2, 0, 1, 0.9);
width: 100%;

@media (min-width: 66em) {
Expand Down
3 changes: 2 additions & 1 deletion src/components/SideNav/_side-nav.scss
Expand Up @@ -43,7 +43,8 @@
padding: 0 $spacing-05;
position: relative;
text-decoration: none;
transition: color 110ms, background-color 110ms, outline 110ms;
transition: color $duration--fast-02, background-color $duration--fast-02,
outline $duration--fast-02;

&:focus {
outline: 2px solid $interactive-01;
Expand Down
2 changes: 1 addition & 1 deletion src/components/TableCard/_table-card.scss
Expand Up @@ -27,7 +27,7 @@

p {
margin-bottom: 8px;
font-size: 14px;
font-size: carbon--type-scale(2);
font-weight: 600;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/TileCatalog/_catalog-content.scss
Expand Up @@ -22,7 +22,7 @@
}

.#{$iot-prefix}--sample-tile-title {
color: #3d70b2;
color: $link-01;
text-overflow: ellipsis;
white-space: nowrap;
overflow-x: hidden;
Expand All @@ -39,7 +39,7 @@
.#{$iot-prefix}--sample-tile-contents {
display: flex;
flex-flow: column nowrap;
padding: 0 1rem;
padding: 0 $spacing-05;
align-self: flex-start;
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/TileCatalog/_tile-group.scss
Expand Up @@ -21,7 +21,7 @@
flex: 1 1 33.33%;
display: none;
min-height: 0px;
padding: 0px;
padding: 0;
border-top: 0px;
border-bottom: 0px;
@media screen and (min-width: $two-pane) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/TileCatalogNew/_tile-catalog.scss
Expand Up @@ -43,7 +43,7 @@
svg {
height: 2.5rem;
width: 2.5rem;
padding: 0.75rem;
padding: $spacing-04;
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/TileGallery/_tile-gallery.scss
Expand Up @@ -169,7 +169,7 @@
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
transition: 0.5s ease;
transition: $duration--slow-01 ease;
opacity: 0;
display: flex;
align-items: center;
Expand Down

0 comments on commit 614112f

Please sign in to comment.