-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(lint-rules): create style lint rules
Create a set of base style lint rules to be used by angular repos
- Loading branch information
1 parent
dc08392
commit 378c3df
Showing
11 changed files
with
714 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
load("//tools:defaults.bzl", "ts_library") | ||
|
||
ts_library( | ||
name = "lib", | ||
srcs = glob(["*.ts"]), | ||
# Note: stylelint rules need to be written in CommonJS. | ||
devmode_module = "commonjs", | ||
visibility = ["//:npm"], | ||
deps = [ | ||
"@npm//@types/node", | ||
"@npm//stylelint", | ||
"@npm//typescript", | ||
], | ||
) | ||
|
||
filegroup( | ||
name = "static_files", | ||
srcs = ["ts-node-loader-rule.js"], | ||
visibility = ["//:npm"], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import stylelint, {Rule} from 'stylelint'; | ||
import {basename} from 'path'; | ||
|
||
const {utils, createPlugin} = stylelint; | ||
|
||
const ruleName = '@angular/no-concrete-rules'; | ||
const messages = utils.ruleMessages(ruleName, { | ||
expectedWithPattern: (pattern) => | ||
`CSS rules must be placed inside a mixin for files matching '${pattern}'.`, | ||
expectedAllFiles: () => `CSS rules must be placed inside a mixin for all files.`, | ||
}); | ||
|
||
/** | ||
* Stylelint plugin that will log a warning for all top-level CSS rules. | ||
* Can be used in theme files to ensure that everything is inside a mixin. | ||
*/ | ||
const ruleFn: Rule<boolean, string> = (isEnabled, options) => { | ||
return (root, result) => { | ||
if (!isEnabled) { | ||
return; | ||
} | ||
|
||
const filePattern = options.filePattern ? new RegExp(options.filePattern) : null; | ||
const fileName = basename(root.source!.input.file!); | ||
|
||
if ((filePattern !== null && !filePattern.test(fileName)) || !root.nodes) { | ||
return; | ||
} | ||
|
||
// Go through all the nodes and report a warning for every CSS rule or mixin inclusion. | ||
// We use a regular `forEach`, instead of the PostCSS walker utils, because we only care | ||
// about the top-level nodes. | ||
root.nodes.forEach((node) => { | ||
if (node.type === 'rule' || (node.type === 'atrule' && node.name === 'include')) { | ||
utils.report({ | ||
result, | ||
ruleName, | ||
node, | ||
message: | ||
filePattern !== null | ||
? messages.expectedWithPattern(filePattern) | ||
: messages.expectedAllFiles(), | ||
}); | ||
} | ||
}); | ||
}; | ||
}; | ||
|
||
ruleFn.ruleName = ruleName; | ||
ruleFn.messages = messages; | ||
|
||
export default createPlugin(ruleName, ruleFn); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import stylelint, {Rule} from 'stylelint'; | ||
import {basename} from 'path'; | ||
|
||
const {utils, createPlugin} = stylelint; | ||
|
||
const ruleName = '@angular/no-import'; | ||
const messages = utils.ruleMessages(ruleName, { | ||
expected: () => '@import is not allowed. Use @use instead.', | ||
}); | ||
|
||
/** Stylelint plugin that doesn't allow `@import` to be used. */ | ||
const ruleFn: Rule<boolean, string> = (isEnabled, options) => { | ||
return (root, result) => { | ||
if (!isEnabled) { | ||
return; | ||
} | ||
|
||
const excludePattern = options?.exclude ? new RegExp(options.exclude) : null; | ||
|
||
if (excludePattern?.test(basename(root.source!.input.file!))) { | ||
return; | ||
} | ||
|
||
root.walkAtRules((rule) => { | ||
if (rule.name === 'import') { | ||
utils.report({ | ||
result, | ||
ruleName, | ||
message: messages.expected(), | ||
node: rule, | ||
}); | ||
} | ||
}); | ||
}; | ||
}; | ||
|
||
ruleFn.ruleName = ruleName; | ||
ruleFn.messages = messages; | ||
|
||
export default createPlugin(ruleName, ruleFn); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import stylelint, {Rule} from 'stylelint'; | ||
import {basename, join} from 'path'; | ||
|
||
const {utils, createPlugin} = stylelint; | ||
|
||
const ruleName = '@angular/no-unused-import'; | ||
const messages = utils.ruleMessages(ruleName, { | ||
expected: (namespace) => `Namespace ${namespace} is not being used.`, | ||
invalid: (rule) => | ||
`Failed to extract namespace from ${rule}. material/no-unused-` + | ||
`imports Stylelint rule likely needs to be updated.`, | ||
}); | ||
|
||
/** Stylelint plugin that flags unused `@use` statements. */ | ||
const ruleFn: Rule<boolean, string> = (isEnabled, _options, context) => { | ||
return (root, result) => { | ||
if (!isEnabled) { | ||
return; | ||
} | ||
|
||
const fileContent = root.toString(); | ||
|
||
root.walkAtRules((rule) => { | ||
if (rule.name === 'use') { | ||
const namespace = extractNamespaceFromUseStatement(rule.params); | ||
|
||
// Flag namespaces we didn't manage to parse so that we can fix the parsing logic. | ||
if (!namespace) { | ||
utils.report({ | ||
result, | ||
ruleName, | ||
message: messages.invalid(rule.params), | ||
node: rule, | ||
}); | ||
} else if (!fileContent.includes(namespace + '.')) { | ||
if (context.fix) { | ||
rule.remove(); | ||
} else { | ||
utils.report({ | ||
result, | ||
ruleName, | ||
message: messages.expected(namespace), | ||
node: rule, | ||
}); | ||
} | ||
} | ||
} | ||
}); | ||
}; | ||
}; | ||
|
||
ruleFn.ruleName = ruleName; | ||
ruleFn.messages = messages; | ||
|
||
/** Extracts the namespace of an `@use` rule from its parameters. */ | ||
function extractNamespaceFromUseStatement(params: string): string | null { | ||
const openQuoteIndex = Math.max(params.indexOf(`"`), params.indexOf(`'`)); | ||
const closeQuoteIndex = Math.max( | ||
params.indexOf(`"`, openQuoteIndex + 1), | ||
params.indexOf(`'`, openQuoteIndex + 1), | ||
); | ||
|
||
if (closeQuoteIndex > -1) { | ||
const asExpression = 'as '; | ||
const asIndex = params.indexOf(asExpression, closeQuoteIndex); | ||
const withIndex = params.indexOf(' with', asIndex); | ||
|
||
// If we found an ` as ` expression, we consider the rest of the text as the namespace. | ||
if (asIndex > -1) { | ||
return withIndex == -1 | ||
? params.slice(asIndex + asExpression.length).trim() | ||
: params.slice(asIndex + asExpression.length, withIndex).trim(); | ||
} | ||
|
||
const importPath = params | ||
.slice(openQuoteIndex + 1, closeQuoteIndex) | ||
// Sass allows for leading underscores to be omitted and it technically supports .scss. | ||
.replace(/^_|(\.import)?\.scss$|\.import$/g, ''); | ||
|
||
// Built-in Sass imports look like `sass:map`. | ||
if (importPath.startsWith('sass:')) { | ||
return importPath.split('sass:')[1]; | ||
} | ||
|
||
// Sass ignores `/index` and infers the namespace as the next segment in the path. | ||
const fileName = basename(importPath); | ||
return fileName === 'index' ? basename(join(fileName, '..')) : fileName; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
export default createPlugin(ruleName, ruleFn); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import stylelint, {Rule} from 'stylelint'; | ||
|
||
const {utils, createPlugin} = stylelint; | ||
|
||
const isStandardSyntaxRule = require('stylelint/lib/utils/isStandardSyntaxRule'); | ||
const isStandardSyntaxSelector = require('stylelint/lib/utils/isStandardSyntaxSelector'); | ||
|
||
const ruleName = '@angular/selector-no-deep'; | ||
const messages = utils.ruleMessages(ruleName, { | ||
expected: (selector) => `Usage of the /deep/ in "${selector}" is not allowed`, | ||
}); | ||
|
||
/** | ||
* Stylelint plugin that prevents uses of /deep/ in selectors. | ||
*/ | ||
const ruleFn: Rule<boolean, unknown> = (isEnabled) => { | ||
return (root, result) => { | ||
if (!isEnabled) { | ||
return; | ||
} | ||
|
||
root.walkRules((rule) => { | ||
if ( | ||
rule.parent?.type === 'rule' && | ||
isStandardSyntaxRule(rule) && | ||
isStandardSyntaxSelector(rule.selector) && | ||
rule.selector.includes('/deep/') | ||
) { | ||
utils.report({ | ||
result, | ||
ruleName, | ||
message: messages.expected(rule.selector), | ||
node: rule, | ||
}); | ||
} | ||
}); | ||
}; | ||
}; | ||
|
||
ruleFn.ruleName = ruleName; | ||
ruleFn.messages = messages; | ||
|
||
export default createPlugin(ruleName, ruleFn); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import stylelint, {Rule} from 'stylelint'; | ||
import {basename} from 'path'; | ||
|
||
const {utils, createPlugin} = stylelint; | ||
|
||
const ruleName = '@angular/single-line-comment-only'; | ||
const messages = utils.ruleMessages(ruleName, { | ||
expected: () => | ||
'Multi-line comments are not allowed (e.g. /* */). ' + 'Use single-line comments instead (//).', | ||
}); | ||
|
||
/** | ||
* Stylelint plugin that doesn't allow multi-line comments to | ||
* be used, because they'll show up in the user's output. | ||
*/ | ||
const ruleFn: Rule<boolean, string> = (isEnabled, options) => { | ||
return (root, result) => { | ||
if (!isEnabled) { | ||
return; | ||
} | ||
|
||
const filePattern = options?.filePattern ? new RegExp(options.filePattern) : null; | ||
|
||
if (filePattern && !filePattern?.test(basename(root.source!.input.file!))) { | ||
return; | ||
} | ||
|
||
root.walkComments((comment) => { | ||
// Allow comments starting with `!` since they're used to tell minifiers to preserve the comment. | ||
if (!comment.raws.inline && !comment.text.startsWith('!')) { | ||
utils.report({ | ||
result, | ||
ruleName, | ||
message: messages.expected(), | ||
node: comment, | ||
}); | ||
} | ||
}); | ||
}; | ||
}; | ||
|
||
ruleFn.ruleName = ruleName; | ||
ruleFn.messages = messages; | ||
|
||
export default createPlugin(ruleName, ruleFn); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
const path = require('path'); | ||
const stylelint = require('stylelint'); | ||
|
||
// Custom rule that registers all of the custom rules, written in TypeScript, with ts-node. | ||
require('ts-node').register(); | ||
|
||
// Dummy rule so Stylelint doesn't complain that there aren't rules in the file. | ||
module.exports = stylelint.createPlugin('@angular/rules-loader', () => {}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.