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(linting): Improve sass linting using stylelint and a custom formatter #1359

Merged
merged 12 commits into from Jul 14, 2020
Merged
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;
}
tay1orjones marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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": [
tay1orjones marked this conversation as resolved.
Show resolved Hide resolved
{
"font-size": [
"em",
"px",
"pt"
],
"margin": [
"px",
"rem"
],
"padding": [
"px",
"rem"
],
"transition": [
"s",
"ms"
]
},
{
"severity": "error"
}
],
"declaration-property-value-blacklist": [
tay1orjones marked this conversation as resolved.
Show resolved Hide resolved
{
"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