From 4b3c213ed0eafdab18c568cd5868de071830ef5a Mon Sep 17 00:00:00 2001 From: David Clark Date: Sun, 15 Jan 2017 11:21:58 -0700 Subject: [PATCH] Fix handling of shared-line comments Introduces a number of utilities and adjusts the following rules: at-rule-empty-line-before, custom-property-empty-line-before, declaration-empty-line-before, rule-nested-empty-line-before, rule-non-nested-empty-line-before Closes #2237; closes #2239; closes #2240. --- decls/postcss.js | 56 ++++++--- jest-setup.js | 54 +++++---- lib/rules/at-rule-empty-line-before/README.md | 26 ++++ .../__tests__/index.js | 81 ++++++++++--- lib/rules/at-rule-empty-line-before/index.js | 47 ++------ lib/rules/checkRuleEmptyLineBefore.js | 20 +++- .../README.md | 37 ++++++ .../__tests__/index.js | 47 ++++++-- .../index.js | 15 +-- .../declaration-empty-line-before/README.md | 37 ++++++ .../__tests__/index.js | 77 ++++++++++-- .../declaration-empty-line-before/index.js | 16 +-- .../rule-nested-empty-line-before/README.md | 22 ++++ .../__tests__/index.js | 64 +++++++++- .../README.md | 29 +++-- .../__tests__/index.js | 14 ++- .../getNextNonSharedLineCommentNode.test.js | 53 ++++++++ ...etPreviousNonSharedLineCommentNode.test.js | 53 ++++++++ .../__tests__/isAfterCommentLine.test.js | 47 ++++++++ ...locklessAtRuleAfterBlocklessAtRule.test.js | 61 ++++++++++ ...AtRuleAfterSameNameBlocklessAtRule.test.js | 69 +++++++++++ .../__tests__/isFirstNestedStatement.test.js | 76 ++++++++++++ .../__tests__/isSharedLineComment.test.js | 113 ++++++++++++++++++ lib/utils/getNextNonSharedLineCommentNode.js | 27 +++++ .../getPreviousNonSharedLineCommentNode.js | 29 +++++ lib/utils/hasBlock.js | 2 +- lib/utils/isAfterCommentLine.js | 14 +++ .../isAfterStandardPropertyDeclaration.js | 16 +++ .../isBlocklessAtRuleAfterBlocklessAtRule.js | 27 +++++ ...klessAtRuleAfterSameNameBlocklessAtRule.js | 23 ++++ lib/utils/isFirstNestedStatement.js | 14 +++ lib/utils/isSharedLineComment.js | 35 ++++++ 32 files changed, 1159 insertions(+), 142 deletions(-) create mode 100644 lib/utils/__tests__/getNextNonSharedLineCommentNode.test.js create mode 100644 lib/utils/__tests__/getPreviousNonSharedLineCommentNode.test.js create mode 100644 lib/utils/__tests__/isAfterCommentLine.test.js create mode 100644 lib/utils/__tests__/isBlocklessAtRuleAfterBlocklessAtRule.test.js create mode 100644 lib/utils/__tests__/isBlocklessAtRuleAfterSameNameBlocklessAtRule.test.js create mode 100644 lib/utils/__tests__/isFirstNestedStatement.test.js create mode 100644 lib/utils/__tests__/isSharedLineComment.test.js create mode 100644 lib/utils/getNextNonSharedLineCommentNode.js create mode 100644 lib/utils/getPreviousNonSharedLineCommentNode.js create mode 100644 lib/utils/isAfterCommentLine.js create mode 100644 lib/utils/isAfterStandardPropertyDeclaration.js create mode 100644 lib/utils/isBlocklessAtRuleAfterBlocklessAtRule.js create mode 100644 lib/utils/isBlocklessAtRuleAfterSameNameBlocklessAtRule.js create mode 100644 lib/utils/isFirstNestedStatement.js create mode 100644 lib/utils/isSharedLineComment.js diff --git a/decls/postcss.js b/decls/postcss.js index b958cdb30d..2d2535a209 100644 --- a/decls/postcss.js +++ b/decls/postcss.js @@ -1,5 +1,10 @@ -export type postcss$comment = { - text: string, +declare class postcss$node { + raw: Function, + type: 'rule' | 'atrule' | 'root' | 'comment' | 'decl'; + parent: Object; + nodes: Array; + next(): postcss$node | void; + prev(): postcss$node | void; source: { start: { line: number, @@ -9,26 +14,41 @@ export type postcss$comment = { line: number, column: number, }, - }, + }; error(message: string, options: { plugin: string }): void, } -export type postcss$atRule = { - name: string, - params: string, - raw: Function, +declare class postcss$comment extends postcss$node { + text: string; + raws: { + before?: string, + after?: string, + }; +} + +declare class postcss$atRule extends postcss$node { + name: string; + params: string; raws: { - afterName: string, - }, - type: string, - parent: Object, - nodes: Array + before?: string, + after?: string, + afterName?: string, + }; } -export type postcss$rule = { - raws: Object, - selector: string, - type: string, - parent: Object, - nodes: Array, +declare class postcss$rule extends postcss$node { + selector: string; + raws: { + before?: string, + after?: string, + }; +} + +declare class postcss$decl extends postcss$node { + prop: string; + value: string; + raws: { + before?: string, + after?: string, + }; } diff --git a/jest-setup.js b/jest-setup.js index 262195d5f9..53ff41c91c 100644 --- a/jest-setup.js +++ b/jest-setup.js @@ -21,13 +21,17 @@ global.testRule = (rule, schema) => { describe("accept", () => { passingTestCases.forEach((testCase) => { const spec = (testCase.only) ? it.only : it - spec(testCase.description || "no description", () => { - return stylelint({ - code: testCase.code, - config: stylelintConfig, - syntax: schema.syntax, - }).then((output) => { - expect(output.results[0].warnings).toEqual([]) + describe(JSON.stringify(schema.config), () => { + describe(JSON.stringify(testCase.code), () => { + spec(testCase.description || "no description", () => { + return stylelint({ + code: testCase.code, + config: stylelintConfig, + syntax: schema.syntax, + }).then((output) => { + expect(output.results[0].warnings).toEqual([]) + }) + }) }) }) }) @@ -38,22 +42,26 @@ global.testRule = (rule, schema) => { describe("reject", () => { schema.reject.forEach((testCase) => { const spec = (testCase.only) ? it.only : it - spec(testCase.description || "no description", () => { - return stylelint({ - code: testCase.code, - config: stylelintConfig, - syntax: schema.syntax, - }).then((output) => { - const warning = output.results[0].warnings[0] - if (testCase.message) { - expect(_.get(warning, "text")).toBe(testCase.message) - } - if (testCase.line) { - expect(_.get(warning, "line")).toBe(testCase.line) - } - if (testCase.column) { - expect(_.get(warning, "column")).toBe(testCase.column) - } + describe(JSON.stringify(schema.config), () => { + describe(JSON.stringify(testCase.code), () => { + spec(testCase.description || "no description", () => { + return stylelint({ + code: testCase.code, + config: stylelintConfig, + syntax: schema.syntax, + }).then((output) => { + const warning = output.results[0].warnings[0] + if (testCase.message) { + expect(_.get(warning, "text")).toBe(testCase.message) + } + if (testCase.line) { + expect(_.get(warning, "line")).toBe(testCase.line) + } + if (testCase.column) { + expect(_.get(warning, "column")).toBe(testCase.column) + } + }) + }) }) }) }) diff --git a/lib/rules/at-rule-empty-line-before/README.md b/lib/rules/at-rule-empty-line-before/README.md index fc78309ae0..3db5c73c31 100644 --- a/lib/rules/at-rule-empty-line-before/README.md +++ b/lib/rules/at-rule-empty-line-before/README.md @@ -108,6 +108,8 @@ Reverse the primary option for blockless at-rules that follow another blockless This means that you can group your blockless at-rules by name. +Shared-line comments do not affect this option. + For example, with `"always"`: The following patterns are *not* considered warnings: @@ -119,6 +121,13 @@ The following patterns are *not* considered warnings: @import url(y.css); ``` +```css +@charset "UTF-8"; + +@import url(x.css); /* comment */ +@import url(y.css); +``` + ```css a { @@ -134,6 +143,8 @@ a { Reverse the primary option for at-rules within a blockless group. +Shared-line comments do not affect this option. + For example, with `"always"`: The following patterns are considered warnings: @@ -155,6 +166,13 @@ The following patterns are *not* considered warnings: @media print {} ``` +```css +@import url(x.css); /* comment */ +@import url(y.css); + +@media print {} +``` + #### `"first-nested"` Reverse the primary option for at-rules that are nested and the first child of their parent node. @@ -197,6 +215,8 @@ b { Ignore at-rules that come after a comment. +Shared-line comments do not trigger this option. + The following patterns are *not* considered warnings: ```css @@ -210,6 +230,12 @@ The following patterns are *not* considered warnings: @media {} ``` +```css +@media {} /* comment */ + +@media {} +``` + #### `"all-nested"` Ignore at-rules that are nested. diff --git a/lib/rules/at-rule-empty-line-before/__tests__/index.js b/lib/rules/at-rule-empty-line-before/__tests__/index.js index 325592c6dd..71f25e25d8 100644 --- a/lib/rules/at-rule-empty-line-before/__tests__/index.js +++ b/lib/rules/at-rule-empty-line-before/__tests__/index.js @@ -123,6 +123,12 @@ testRule(rule, mergeTestDescriptions(sharedAlwaysTests, { }, { code: "@import 'x.css';", description: "single blockless rule", + }, { + code: stripIndent` + @charset "UTF-8"; + @import url(x.css); /* comment */ + @import url(y.css);`, + description: "shared-line comment accepted", } ], reject: [ { @@ -138,13 +144,27 @@ testRule(rule, { ruleName, config: [ "always", { ignore: ["blockless-group"] } ], - accept: [{ - code: "@media {}; @import 'x.css';", - }], + accept: [ { + code: "@import 'y.css'; @import 'x.css';", + }, { + code: stripIndent` + @charset "UTF-8"; + + @import url(x.css); /* comment */ + @import url(y.css);`, + description: "shared-line comment accepted", + } ], reject: [ { code: "@import 'x.css'; @media {};", message: messages.expected, + line: 1, + column: 18, + }, { + code: "@media {}; @import 'x.css';", + message: messages.expected, + line: 1, + column: 12, }, { code: "@import 'test'; @include mixin(1) { @content; };", message: messages.expected, @@ -164,10 +184,16 @@ testRule(rule, { description: "CRLF", } ], - reject: [{ + reject: [ { code: "a {} @media {}", message: messages.expected, - }], + }, { + code: "bar {} /* foo */\n@media {}", + message: messages.expected, + line: 2, + column: 1, + description: "after shared-line comment", + } ], }) testRule(rule, mergeTestDescriptions(sharedAlwaysTests, { @@ -332,21 +358,15 @@ testRule(rule, { ruleName, config: [ "never", { ignore: ["blockless-group"] } ], - accept: [ { - code: ` - @media {}; - - @import 'x.css'; - `, - }, { + accept: [{ code: ` @import 'x.css'; @import 'y.css'; `, - } ], + }], - reject: [{ + reject: [ { code: ` @import 'x.css'; @@ -355,7 +375,16 @@ testRule(rule, { message: messages.rejected, line: 4, column: 7, - }], + }, { + code: ` + @media {}; + + @import 'x.css'; + `, + message: messages.rejected, + line: 4, + column: 7, + } ], }) testRule(rule, { @@ -378,6 +407,12 @@ testRule(rule, { code: "b {}\r\n\r\n@media {}", description: "CRLF", message: messages.rejected, + }, { + code: "b {} /* comment */\n\n@media {}", + description: "after shared-line comment", + message: messages.rejected, + line: 3, + column: 1, } ], }) @@ -578,6 +613,13 @@ testRule(rule, mergeTestDescriptions(sharedAlwaysTests, { @include loop; @include doo; }`, + }, { + code: stripIndent` + @charset "UTF-8"; + + @import url(x.css); /* comment */ + @import url(y.css);`, + description: "shared-line comment accepted", } ], reject: [ { @@ -625,6 +667,13 @@ testRule(rule, mergeTestDescriptions(sharedAlwaysTests, { @include loop; @include doo; }`, + }, { + code: stripIndent` + @charset "UTF-8"; + + @import url(x.css); /* comment */ + @import url(y.css);`, + description: "shared-line comment accepted", } ], reject: [ { @@ -660,7 +709,7 @@ testRule(rule, mergeTestDescriptions(sharedNeverTests, { code: stripIndent` @charset "UTF-8"; @import url(x.css); - + @import url(y.css);`, }, { code: stripIndent` diff --git a/lib/rules/at-rule-empty-line-before/index.js b/lib/rules/at-rule-empty-line-before/index.js index cb387ef4b5..37e65527a1 100644 --- a/lib/rules/at-rule-empty-line-before/index.js +++ b/lib/rules/at-rule-empty-line-before/index.js @@ -1,8 +1,11 @@ "use strict" const _ = require("lodash") -const hasBlock = require("../../utils/hasBlock") const hasEmptyLine = require("../../utils/hasEmptyLine") +const isAfterCommentLine = require("../../utils/isAfterCommentLine") +const isBlocklessAtRuleAfterBlocklessAtRule = require("../../utils/isBlocklessAtRuleAfterBlocklessAtRule") +const isBlocklessAtRuleAfterSameNameBlocklessAtRule = require("../../utils/isBlocklessAtRuleAfterSameNameBlocklessAtRule") +const isFirstNestedStatement = require("../../utils/isFirstNestedStatement") const optionsMatches = require("../../utils/optionsMatches") const report = require("../../utils/report") const ruleMessages = require("../../utils/ruleMessages") @@ -47,6 +50,8 @@ const rule = function (expectation, options) { } root.walkAtRules(atRule => { + const isNested = atRule.parent !== root + // Ignore the first node if (atRule === root.first) { return @@ -60,19 +65,16 @@ const rule = function (expectation, options) { // Optionally ignore the expectation if the node is blockless if ( optionsMatches(options, "ignore", "blockless-group") - && !hasBlock(atRule) + && isBlocklessAtRuleAfterBlocklessAtRule(atRule) ) { return } - const isNested = atRule.parent !== root - const previousNode = atRule.prev() - // Optionally ignore the expection if the node is blockless // and following another blockless at-rule with the same name if ( optionsMatches(options, "ignore", "blockless-after-same-name-blockless") - && isBlocklessAfterSameNameBlockless() + && isBlocklessAtRuleAfterSameNameBlocklessAtRule(atRule) ) { return } @@ -88,7 +90,7 @@ const rule = function (expectation, options) { // Optionally ignore the expectation if a comment precedes this node if ( optionsMatches(options, "ignore", "after-comment") - && isAfterComment() + && isAfterCommentLine(atRule) ) { return } @@ -103,11 +105,11 @@ const rule = function (expectation, options) { optionsMatches(options, "except", "all-nested") && isNested || optionsMatches(options, "except", "first-nested") - && isFirstNested() + && isFirstNestedStatement(atRule) || optionsMatches(options, "except", "blockless-group") - && isBlocklessAfterBlockless() + && isBlocklessAtRuleAfterBlocklessAtRule(atRule) || optionsMatches(options, "except", "blockless-after-same-name-blockless") - && isBlocklessAfterSameNameBlockless() + && isBlocklessAtRuleAfterSameNameBlocklessAtRule(atRule) ) { expectEmptyLineBefore = !expectEmptyLineBefore } @@ -122,31 +124,6 @@ const rule = function (expectation, options) { : messages.rejected report({ message, node: atRule, result, ruleName }) - - function isAfterComment() { - return previousNode - && previousNode.type === "comment" - } - - function isBlocklessAfterBlockless() { - return previousNode - && previousNode.type === "atrule" - && !hasBlock(previousNode) - && !hasBlock(atRule) - } - - function isBlocklessAfterSameNameBlockless() { - return !hasBlock(atRule) - && previousNode - && !hasBlock(previousNode) - && previousNode.type === "atrule" - && previousNode.name == atRule.name - } - - function isFirstNested() { - return isNested - && atRule === atRule.parent.first - } }) } } diff --git a/lib/rules/checkRuleEmptyLineBefore.js b/lib/rules/checkRuleEmptyLineBefore.js index 9910d29b22..d4e1c48677 100644 --- a/lib/rules/checkRuleEmptyLineBefore.js +++ b/lib/rules/checkRuleEmptyLineBefore.js @@ -1,15 +1,27 @@ "use strict" +const _ = require("lodash") +const getPreviousNonSharedLineCommentNode = require("../utils/getPreviousNonSharedLineCommentNode") const hasEmptyLine = require("../utils/hasEmptyLine") +const isAfterCommentLine = require("../utils/isAfterCommentLine") +const isFirstNestedStatement = require("../utils/isFirstNestedStatement") const isSingleLineString = require("../utils/isSingleLineString") const optionsMatches = require("../utils/optionsMatches") const report = require("../utils/report") -module.exports = function (opts) { +module.exports = function (opts/*: { + expectation: string, + options?: Object, + rule: postcss$rule, + messages: { + expected: string, + rejected: string, + }, +}*/) { let expectEmptyLineBefore = opts.expectation.indexOf("always") !== -1 ? true : false // Optionally ignore the expectation if a comment precedes this node - if (optionsMatches(opts.options, "ignore", "after-comment") && opts.rule.prev() && opts.rule.prev().type === "comment") { + if (optionsMatches(opts.options, "ignore", "after-comment") && isAfterCommentLine(opts.rule)) { return } @@ -19,12 +31,12 @@ module.exports = function (opts) { } // Optionally reverse the expectation for the first nested node - if (optionsMatches(opts.options, "except", "first-nested") && opts.rule === opts.rule.parent.first) { + if (optionsMatches(opts.options, "except", "first-nested") && isFirstNestedStatement(opts.rule)) { expectEmptyLineBefore = !expectEmptyLineBefore } // Optionally reverse the expectation if a rule precedes this node - if (optionsMatches(opts.options, "except", "after-rule") && opts.rule.prev() && opts.rule.prev().type === "rule") { + if (optionsMatches(opts.options, "except", "after-rule") && _.get(getPreviousNonSharedLineCommentNode(opts.rule), "type") === "rule") { expectEmptyLineBefore = !expectEmptyLineBefore } diff --git a/lib/rules/custom-property-empty-line-before/README.md b/lib/rules/custom-property-empty-line-before/README.md index 827b3975ba..786ae5269d 100644 --- a/lib/rules/custom-property-empty-line-before/README.md +++ b/lib/rules/custom-property-empty-line-before/README.md @@ -87,6 +87,8 @@ a { Reverse the primary option for custom properties that come after a comment. +Shared-line comments do not trigger this option. + For example, with `"always"`: The following patterns are considered warnings: @@ -101,6 +103,14 @@ a { } ``` +```css +a { + + --foo: pink; /* comment */ + --bar: red; +} +``` + The following patterns are *not* considered warnings: ```css @@ -110,13 +120,23 @@ a { /* comment */ --bar: red; } +``` + +```css +a { + --foo: pink; /* comment */ + + --bar: red; +} ``` #### `"after-custom-property"` Reverse the primary option for custom properties that come after another custom property. +Shared-line comments do not affect this option. + For example, with `"always"`: The following patterns are considered warnings: @@ -130,6 +150,15 @@ a { } ``` +```css +a { + + --foo: pink; /* comment */ + + --bar: red; +} +``` + The following patterns are *not* considered warnings: ```css @@ -140,6 +169,14 @@ a { } ``` +```css +a { + + --foo: pink; /* comment */ + --bar: red; +} +``` + #### `"first-nested"` Reverse the primary option for custom properties that are nested and the first child of their parent node. diff --git a/lib/rules/custom-property-empty-line-before/__tests__/index.js b/lib/rules/custom-property-empty-line-before/__tests__/index.js index 735f29d551..88e3d626d8 100644 --- a/lib/rules/custom-property-empty-line-before/__tests__/index.js +++ b/lib/rules/custom-property-empty-line-before/__tests__/index.js @@ -71,19 +71,29 @@ testRule(rule, { config: [ "always", { ignore: ["after-comment"] } ], accept: [ { - code: "a {\n/* comment */ --custom-prop: value;\n}", - }, { code: "a {\n/* comment */\n--custom-prop: value;\n}", }, { code: "a {\r\n/* comment */\r\nn--custom-prop: value;\r\n}", } ], - reject: [{ + reject: [ { code: "a {\n --custom-prop: value;\n}", message: messages.expected, line: 2, column: 2, - }], + }, { + code: "a {\ncolor: pink; /* comment */\n--custom-prop: value;\n}", + message: messages.expected, + line: 3, + column: 1, + description: "shared-line comments do not apply", + }, { + code: "a {\n/* comment */ --custom-prop: value;\n}", + message: messages.expected, + line: 2, + column: 15, + description: "shared-line comments do not apply", + } ], }) testRule(rule, { @@ -136,9 +146,9 @@ testRule(rule, { accept: [ { code: "a {\n\n --custom-prop: value;\n}", }, { - code: "a {/* I am a comment */ \n --custom-prop2: value;}", + code: "a {\n\n /* I am a comment */ \n --custom-prop2: value;}", }, { - code: "a {/* I am a comment */ \r\n --custom-prop2: value;}", + code: "a {\r\n /* I am a comment */ \r\n --custom-prop2: value;}", } ], reject: [ { @@ -151,6 +161,18 @@ testRule(rule, { message: messages.rejected, line: 4, column: 2, + }, { + code: "a {\ncolor: pink; /* I am a comment */\n--custom-prop2: value;}", + message: messages.expected, + line: 3, + column: 1, + description: "shared-line comments do not apply", + }, { + code: "a {/* I am a comment */ \n --custom-prop2: value;}", + message: messages.expected, + line: 2, + column: 2, + description: "shared-line comments still do not apply", } ], }) @@ -266,9 +288,9 @@ testRule(rule, { accept: [ { code: "a {\n --custom-prop: value;\n}", }, { - code: "a {/* I am a comment */ \n\n --custom-prop2: value;}", + code: "a {\n /* I am a comment */ \n\n --custom-prop2: value;}", }, { - code: "a {/* I am a comment */ \r\n\r\n --custom-prop2: value;}", + code: "a {\r\n /* I am a comment */ \r\n\r\n --custom-prop2: value;}", } ], reject: [ { @@ -281,6 +303,12 @@ testRule(rule, { message: messages.expected, line: 3, column: 2, + }, { + code: "a {/* I am a comment */ \n\n --custom-prop2: value;}", + message: messages.rejected, + line: 3, + column: 2, + description: "shared-line comments do not apply", } ], }) @@ -292,6 +320,9 @@ testRule(rule, { code: "a {\n --custom-prop: value;\n}", }, { code: "a {\n --custom-prop:value; \n\n --custom-prop2: value;}", + }, { + code: "a {\n --custom-prop:value; /* comment */\n\n --custom-prop2: value;}", + description: "shared-line comments accepted", }, { code: "a {\n --custom-prop:value; \r\n\r\n --custom-prop2: value;}", } ], diff --git a/lib/rules/custom-property-empty-line-before/index.js b/lib/rules/custom-property-empty-line-before/index.js index 1be5229a34..aff5b4e4ae 100644 --- a/lib/rules/custom-property-empty-line-before/index.js +++ b/lib/rules/custom-property-empty-line-before/index.js @@ -1,7 +1,9 @@ "use strict" const blockString = require("../../utils/blockString") +const getPreviousNonSharedLineCommentNode = require("../../utils/getPreviousNonSharedLineCommentNode") const hasEmptyLine = require("../../utils/hasEmptyLine") +const isAfterCommentLine = require("../../utils/isAfterCommentLine") const isCustomProperty = require("../../utils/isCustomProperty") const isSingleLineString = require("../../utils/isSingleLineString") const isStandardSyntaxDeclaration = require("../../utils/isStandardSyntaxDeclaration") @@ -58,8 +60,7 @@ const rule = function (expectation, options) { // Optionally ignore the node if a comment precedes it if ( optionsMatches(options, "ignore", "after-comment") - && decl.prev() - && decl.prev().type === "comment" + && isAfterCommentLine(decl) ) { return } @@ -85,18 +86,18 @@ const rule = function (expectation, options) { // Optionally reverse the expectation if a comment precedes this node if ( optionsMatches(options, "except", "after-comment") - && decl.prev() - && decl.prev().type === "comment" + && isAfterCommentLine(decl) ) { expectEmptyLineBefore = !expectEmptyLineBefore } // Optionally reverse the expectation if a custom property precedes this node + const prevNode = getPreviousNonSharedLineCommentNode(decl) if ( optionsMatches(options, "except", "after-custom-property") - && decl.prev() - && decl.prev().prop - && isCustomProperty(decl.prev().prop) + && prevNode + && prevNode.prop + && isCustomProperty(prevNode.prop) ) { expectEmptyLineBefore = !expectEmptyLineBefore } diff --git a/lib/rules/declaration-empty-line-before/README.md b/lib/rules/declaration-empty-line-before/README.md index 877a05e73f..d7c9403bd2 100644 --- a/lib/rules/declaration-empty-line-before/README.md +++ b/lib/rules/declaration-empty-line-before/README.md @@ -98,6 +98,8 @@ a { Reverse the primary option for declarations that come after a comment. +Shared-line comments do not trigger this option. + For example, with `"always"`: The following patterns are considered warnings: @@ -110,6 +112,13 @@ a { } ``` +```css +a { + bottom: 5px; /* comment */ + top: 5px; +} +``` + The following patterns are *not* considered warnings: ```css @@ -120,10 +129,21 @@ a { ``` +```css +a { + bottom: 5px; /* comment */ + + top: 5px; +} + +``` + #### `"after-declaration"` Reverse the primary option for declarations that come after another declaration. +Shared-line comments do not affect this option. + For example, with `"always"`: The following patterns are considered warnings: @@ -137,6 +157,15 @@ a { } ``` +```css +a { + + bottom: 15px; /* comment */ + + top: 5px; +} +``` + The following patterns are *not* considered warnings: ```css @@ -147,6 +176,14 @@ a { } ``` +```css +a { + + bottom: 15px; /* comment */ + top: 5px; +} +``` + #### `"first-nested"` Reverse the primary option for declarations that are nested and the first child of their parent node. diff --git a/lib/rules/declaration-empty-line-before/__tests__/index.js b/lib/rules/declaration-empty-line-before/__tests__/index.js index d022206606..7f9cc40edb 100644 --- a/lib/rules/declaration-empty-line-before/__tests__/index.js +++ b/lib/rules/declaration-empty-line-before/__tests__/index.js @@ -173,19 +173,29 @@ testRule(rule, { config: [ "always", { ignore: ["after-comment"] } ], accept: [ { - code: "a {\n/* comment */ top: 15px;\n}", - }, { code: "a {\n/* comment */\ntop: 15px;\n}", }, { code: "a {\r\n/* comment */\r\ntop: 15px;\r\n}", } ], - reject: [{ + reject: [ { + code: "a {\n/* comment */ top: 15px;\n}", + message: messages.expected, + line: 2, + column: 15, + description: "shared-line comments don't save you", + }, { + code: "a {/* comment */\n top: 15px;\n}", + message: messages.expected, + line: 2, + column: 2, + description: "shared-line comments don't save you, again", + }, { code: "a {\n top: 15px;\n}", message: messages.expected, line: 2, column: 2, - }], + } ], }) testRule(rule, { @@ -198,6 +208,9 @@ testRule(rule, { code: "a {\r\n\r\n top: 15px; bottom: 5px;\r\n}", }, { code: "a {\n\n top: 15px;\n bottom: 5px;\n}", + }, { + code: "a {\n\n top: 15px; /* comment */\n bottom: 5px;\n}", + description: "shared-line comment accepted", }, { code: "a {\r\n\r\n top: 15px;\r\n bottom: 5px;\r\n}", }, { @@ -236,6 +249,12 @@ testRule(rule, { message: messages.expected, line: 2, column: 2, + }, { + code: "a {\n --foo: pink;\n top: 15px;\n}", + message: messages.expected, + line: 3, + column: 3, + description: "custom properties don't count", } ], }) @@ -271,9 +290,9 @@ testRule(rule, { accept: [ { code: "a {\n\n top: 15px;\n}", }, { - code: "a {/* I am a comment */ \n bottom: 5px;}", + code: "a {\n /* I am a comment */ \n bottom: 5px;}", }, { - code: "a {/* I am a comment */ \r\n bottom: 5px;}", + code: "a {\r\n /* I am a comment */ \r\n bottom: 5px;}", } ], reject: [ { @@ -286,6 +305,18 @@ testRule(rule, { message: messages.rejected, line: 4, column: 2, + }, { + code: "a {\n\n color: pink; /* I am a comment */\n bottom: 5px;}", + message: messages.expected, + line: 4, + column: 2, + description: "shared-line comments do not apply", + }, { + code: "a {/* I am a comment */ \n bottom: 5px;}", + message: messages.expected, + line: 2, + column: 2, + description: "shared-line comments still do not apply", } ], }) @@ -297,6 +328,9 @@ testRule(rule, { code: "a {\n\n top: 15px;\n}", }, { code: "a {\n\n top:15px; \n bottom: 5px;}", + }, { + code: "a {\n\n top:15px; \n /* comment */ bottom: 5px;}", + description: "shared-line comment accepted", }, { code: "a {\n\n top:15px; \r\n bottom: 5px;}", } ], @@ -311,6 +345,12 @@ testRule(rule, { message: messages.rejected, line: 5, column: 2, + }, { + code: "a {\n --foo: pink;\n bottom: 5px;}", + message: messages.expected, + line: 3, + column: 3, + description: "custom properties don't count", } ], }) @@ -335,6 +375,12 @@ testRule(rule, { message: messages.rejected, line: 5, column: 2, + }, { + code: "a {\n $foo: pink;\n bottom: 5px;}", + message: messages.expected, + line: 3, + column: 3, + description: "variable declarations don't count", } ], }) @@ -464,9 +510,9 @@ testRule(rule, { accept: [ { code: "a {\n top: 15px;\n}", }, { - code: "a {/* I am a comment */ \n\n bottom: 5px;}", + code: "a {\n /* I am a comment */ \n\n bottom: 5px;}", }, { - code: "a {/* I am a comment */ \r\n\r\n bottom: 5px;}", + code: "a {\r\n /* I am a comment */ \r\n\r\n bottom: 5px;}", } ], reject: [ { @@ -484,6 +530,18 @@ testRule(rule, { message: messages.expected, line: 3, column: 2, + }, { + code: "a {\n color: pink; /* I am a comment */\n\n bottom: 5px;}", + message: messages.rejected, + line: 4, + column: 2, + description: "shared-line comments do not apply", + }, { + code: "a {/* I am a comment */ \n\n bottom: 5px;}", + message: messages.rejected, + line: 3, + column: 2, + description: "shared-line comments still do not apply", } ], }) @@ -495,6 +553,9 @@ testRule(rule, { code: "a {\n top: 15px;\n}", }, { code: "a {\n top:15px; \n\n bottom: 5px;}", + }, { + code: "a {\n top:15px; /* comment */\n\n bottom: 5px;}", + description: "shared-line comment accepted", }, { code: "a {\n top:15px; \r\n\r\n bottom: 5px;}", } ], diff --git a/lib/rules/declaration-empty-line-before/index.js b/lib/rules/declaration-empty-line-before/index.js index 0d3d30e3fd..73415d7ddc 100644 --- a/lib/rules/declaration-empty-line-before/index.js +++ b/lib/rules/declaration-empty-line-before/index.js @@ -2,6 +2,8 @@ const blockString = require("../../utils/blockString") const hasEmptyLine = require("../../utils/hasEmptyLine") +const isAfterCommentLine = require("../../utils/isAfterCommentLine") +const isAfterStandardPropertyDeclaration = require("../../utils/isAfterStandardPropertyDeclaration") const isCustomProperty = require("../../utils/isCustomProperty") const isSingleLineString = require("../../utils/isSingleLineString") const isStandardSyntaxDeclaration = require("../../utils/isStandardSyntaxDeclaration") @@ -59,8 +61,7 @@ const rule = function (expectation, options) { // Optionally ignore the node if a comment precedes it if ( optionsMatches(options, "ignore", "after-comment") - && decl.prev() - && decl.prev().type === "comment" + && isAfterCommentLine(decl) ) { return } @@ -68,8 +69,7 @@ const rule = function (expectation, options) { // Optionally ignore the node if a declaration precedes it if ( optionsMatches(options, "ignore", "after-declaration") - && decl.prev() - && decl.prev().type === "decl" + && isAfterStandardPropertyDeclaration(decl) ) { return } @@ -97,8 +97,7 @@ const rule = function (expectation, options) { // Optionally reverse the expectation if a comment precedes this node if ( optionsMatches(options, "except", "after-comment") - && decl.prev() - && decl.prev().type === "comment" + && isAfterCommentLine(decl) ) { expectEmptyLineBefore = !expectEmptyLineBefore } @@ -106,10 +105,7 @@ const rule = function (expectation, options) { // Optionally reverse the expectation if a declaration precedes this node if ( optionsMatches(options, "except", "after-declaration") - && decl.prev() - && decl.prev().prop - && isStandardSyntaxDeclaration(decl.prev()) - && !isCustomProperty(decl.prev().prop) + && isAfterStandardPropertyDeclaration(decl) ) { expectEmptyLineBefore = !expectEmptyLineBefore } diff --git a/lib/rules/rule-nested-empty-line-before/README.md b/lib/rules/rule-nested-empty-line-before/README.md index 2eabb33aa8..555a8d20be 100644 --- a/lib/rules/rule-nested-empty-line-before/README.md +++ b/lib/rules/rule-nested-empty-line-before/README.md @@ -185,6 +185,20 @@ The following patterns are *not* considered warnings: Ignore rules that come after a comment. +Shared-line comments do not trigger this option. + +For example, with `"always"`: + + +The following patterns are considered warnings: + +```css +@media { + b {} /* comment */ + a {} +} +``` + The following patterns are *not* considered warnings: ```css @@ -201,3 +215,11 @@ The following patterns are *not* considered warnings: a {} } ``` + +```css +@media { + b {} /* comment */ + + a {} +} +``` diff --git a/lib/rules/rule-nested-empty-line-before/__tests__/index.js b/lib/rules/rule-nested-empty-line-before/__tests__/index.js index bd4c43a261..6322511236 100644 --- a/lib/rules/rule-nested-empty-line-before/__tests__/index.js +++ b/lib/rules/rule-nested-empty-line-before/__tests__/index.js @@ -133,6 +133,30 @@ testRule(rule, { }, { code: "a {\n color: pink; \n\n b {color: red; } \n c {color: blue; }\n}", description: "css property", + }, { + code: `a { + color: pink; + + b { color: red; } + c { color: blue; } + }`, + description: "no empty line after another nested rule", + }, { + code: `a { + color: pink; + + b { color: red; } /* comment */ + c { color: blue; } + }`, + description: "shared-line comment skipped", + }, { + code: `a { + color: pink; + + b { color: red; } + /* comment */ c { color: blue; } + }`, + description: "shared-line comment skipped", }, { code: "a {\n $var: pink; \n\n b {color: red; } \n c {color: blue; }\n}", description: "scss variable", @@ -187,6 +211,10 @@ testRule(rule, { reject: [ { code: "@media {\n\n a{}\n b {}\n\n}", message: messages.expected, + }, { + code: "@media {\n\n a{} /* comment */\n b {}\n\n}", + message: messages.expected, + description: "inline comment does not save you", }, { code: "@media {\r\n\r\n a{}\r\n b {}\r\n\r\n}", description: "CRLF", @@ -286,8 +314,29 @@ testRule(rule, { code: "a {}\nb {}", description: "non-nested node ignored", }, { - code: "a {\n color: pink; \n b {color: red; } \n\n c {color: blue; }\n}", - description: "css property", + code: `a { + color: pink; + b {color: red; } + + c {color: blue; } + }`, + description: "empty line before nested rule following nested rule", + }, { + code: `a { + color: pink; + b {color: red; } /* comment */ + + c {color: blue; } + }`, + description: "shared-line comment skipped", + }, { + code: `a { + color: pink; + b {color: red; } + /* comment */ + c {color: blue; } + }`, + description: "comment between nested rules, no empty lines", }, { code: "a {\n $var: pink; \n b {color: red; } \n\n c {color: blue; }\n}", description: "scss variable", @@ -316,6 +365,17 @@ testRule(rule, { code: "a {\r\n color: pink;\r\n\r\n b {color: red; }\r\n\r\n c {color: blue; }\r\n}", description: "CRLF", message: messages.rejected, + }, { + code: `a { + color: pink; + b {color: red; } + + /* comment */c {color: blue; } + }`, + description: "no shared-line comment before", + message: messages.expected, + line: 5, + column: 20, } ], }) diff --git a/lib/rules/rule-non-nested-empty-line-before/README.md b/lib/rules/rule-non-nested-empty-line-before/README.md index 9da36fffd6..0294ff0af2 100644 --- a/lib/rules/rule-non-nested-empty-line-before/README.md +++ b/lib/rules/rule-non-nested-empty-line-before/README.md @@ -114,23 +114,36 @@ b Ignore rules that come after a comment. +Shared-line comments do not trigger this option. + +For example, with "always": + +The following patterns are considered warnings: + +```css +a {} /* comment */ +b {} +``` + The following patterns are *not* considered warnings: ```css -a -{} +a {} /* comment */ -b -{} +b {} ``` ```css -a -{} +a {} /* comment */ -b -{} +b {} +``` + +```css +a {} /* comment */ + +b {} ``` ## Optional secondary options diff --git a/lib/rules/rule-non-nested-empty-line-before/__tests__/index.js b/lib/rules/rule-non-nested-empty-line-before/__tests__/index.js index 2bdc057e50..2a17be3fed 100644 --- a/lib/rules/rule-non-nested-empty-line-before/__tests__/index.js +++ b/lib/rules/rule-non-nested-empty-line-before/__tests__/index.js @@ -83,10 +83,20 @@ testRule(rule, { description: "CRLF", } ], - reject: [{ + reject: [ { code: "b {} a {}", message: messages.expected, - }], + description: "no line-break = no good", + }, + { + code: "b {}\na {}", + message: messages.expected, + description: "line-break is not enough", + }, { + code: "b {} /* comment */,\na {}", + message: messages.expected, + description: "shared-line comment does not trigger option", + } ], }) testRule(rule, { diff --git a/lib/utils/__tests__/getNextNonSharedLineCommentNode.test.js b/lib/utils/__tests__/getNextNonSharedLineCommentNode.test.js new file mode 100644 index 0000000000..b59c639bbb --- /dev/null +++ b/lib/utils/__tests__/getNextNonSharedLineCommentNode.test.js @@ -0,0 +1,53 @@ +/* @flow */ +"use strict" + +const getNextNonSharedLineCommentNode = require("../getNextNonSharedLineCommentNode") +const postcss = require("postcss") + +describe("getNextNonSharedLineCommentNode", () => { + it("returns undefined if there is no next node", () => { + const root = postcss.parse(` + a {} + `) + expect(getNextNonSharedLineCommentNode(root.nodes[0])).toBe(undefined) + }) + + it("returns undefined if there is no next node that's not a shared-line comment", () => { + const root = postcss.parse(` + a {} /* comment */ + `) + expect(getNextNonSharedLineCommentNode(root.nodes[0])).toBe(undefined) + }) + + it("returns the next node if it is not a shared-line comment", () => { + const root = postcss.parse(` + a {} + b {} + `) + expect(getNextNonSharedLineCommentNode(root.nodes[0])).toBe(root.nodes[1]) + }) + + it("returns the next node after a shared-line comment at the end of its line", () => { + const root = postcss.parse(` + a {} /* comment */ + b {} + `) + expect(getNextNonSharedLineCommentNode(root.nodes[0])).toBe(root.nodes[2]) + }) + + it("returns the next node after a shared-line comment at the beginning of its line", () => { + const root = postcss.parse(` + a {} + /* comment */ b {} + `) + expect(getNextNonSharedLineCommentNode(root.nodes[0])).toBe(root.nodes[2]) + }) + + it("returns the next node after two shared-line comments", () => { + const root = postcss.parse(` + a {} /* comment */ + /* comment */ b {} + `) + expect(getNextNonSharedLineCommentNode(root.nodes[0])).toBe(root.nodes[3]) + }) +}) diff --git a/lib/utils/__tests__/getPreviousNonSharedLineCommentNode.test.js b/lib/utils/__tests__/getPreviousNonSharedLineCommentNode.test.js new file mode 100644 index 0000000000..b52bce698d --- /dev/null +++ b/lib/utils/__tests__/getPreviousNonSharedLineCommentNode.test.js @@ -0,0 +1,53 @@ +/* @flow */ +"use strict" + +const getPreviousNonSharedLineCommentNode = require("../getPreviousNonSharedLineCommentNode") +const postcss = require("postcss") + +describe("getPreviousNonSharedLineCommentNode", () => { + it("returns undefined if there is no node before", () => { + const root = postcss.parse(` + a {} + `) + expect(getPreviousNonSharedLineCommentNode(root.nodes[0])).toBe(undefined) + }) + + it("returns undefined if there is no node before the prior shared-line comment", () => { + const root = postcss.parse(` + /* comment */ a {} + `) + expect(getPreviousNonSharedLineCommentNode(root.nodes[0])).toBe(undefined) + }) + + it("returns the previous node if it is not a shared-line comment", () => { + const root = postcss.parse(` + a {} + b {} + `) + expect(getPreviousNonSharedLineCommentNode(root.nodes[1])).toBe(root.nodes[0]) + }) + + it("returns the node before a prior shared-line comment at the end of its line", () => { + const root = postcss.parse(` + a {} /* comment */ + b {} + `) + expect(getPreviousNonSharedLineCommentNode(root.nodes[2])).toBe(root.nodes[0]) + }) + + it("returns the node before a prior shared-line comment at the beginning of its line", () => { + const root = postcss.parse(` + a {} + /* comment */ b {} + `) + expect(getPreviousNonSharedLineCommentNode(root.nodes[2])).toBe(root.nodes[0]) + }) + + it("returns the node before two shared-line comments", () => { + const root = postcss.parse(` + a {} /* comment */ + /* comment */ b {} + `) + expect(getPreviousNonSharedLineCommentNode(root.nodes[3])).toBe(root.nodes[0]) + }) +}) diff --git a/lib/utils/__tests__/isAfterCommentLine.test.js b/lib/utils/__tests__/isAfterCommentLine.test.js new file mode 100644 index 0000000000..d7ef144bba --- /dev/null +++ b/lib/utils/__tests__/isAfterCommentLine.test.js @@ -0,0 +1,47 @@ +/* flow */ +"use strict" + +const isAfterCommentLine = require("../isAfterCommentLine") +const postcss = require("postcss") + +describe("isAfterCommentLine", () => { + it("returns true when after a single-line comment", () => { + const root = postcss.parse(` + /* comment */ + foo {} + `) + expect(isAfterCommentLine(root.nodes[1])).toBe(true) + }) + + it("returns true when after a multi-line comment", () => { + const root = postcss.parse(` + /* comment + and more comment */ + foo {} + `) + expect(isAfterCommentLine(root.nodes[1])).toBe(true) + }) + + it("returns false when after a shared-line comment", () => { + const root = postcss.parse(` + bar {} /* comment */ + foo {} + `) + expect(isAfterCommentLine(root.nodes[2])).toBe(false) + }) + + it("returns false when after a non-comment node", () => { + const root = postcss.parse(` + bar {} + foo {} + `) + expect(isAfterCommentLine(root.nodes[1])).toBe(false) + }) + + it("returns false when after no nodes", () => { + const root = postcss.parse(` + foo {} + `) + expect(isAfterCommentLine(root.nodes[0])).toBe(false) + }) +}) diff --git a/lib/utils/__tests__/isBlocklessAtRuleAfterBlocklessAtRule.test.js b/lib/utils/__tests__/isBlocklessAtRuleAfterBlocklessAtRule.test.js new file mode 100644 index 0000000000..2dfa3b3ac0 --- /dev/null +++ b/lib/utils/__tests__/isBlocklessAtRuleAfterBlocklessAtRule.test.js @@ -0,0 +1,61 @@ +/* @flow */ +"use strict" + +const isBlocklessAtRuleAfterBlocklessAtRule = require("../isBlocklessAtRuleAfterBlocklessAtRule") +const postcss = require("postcss") + +describe("isBlocklessAtRuleAfterBlocklessAtRule", () => { + it("returns false with the first node", () => { + const root = postcss.parse(` + @import 'x.css' + `) + expect(isBlocklessAtRuleAfterBlocklessAtRule(root.nodes[0])).toBe(false) + }) + + it("returns false with a non-at-rule", () => { + const root = postcss.parse(` + foo {} + `) + expect(isBlocklessAtRuleAfterBlocklessAtRule(root.nodes[0])).toBe(false) + }) + + it("returns false when blockless-at-rule follows a non-at-rule", () => { + const root = postcss.parse(` + foo {} + @import 'x.css'; + `) + expect(isBlocklessAtRuleAfterBlocklessAtRule(root.nodes[1])).toBe(false) + }) + + it("returns false when blockless-at-rule follows a non-blockless-at-rule", () => { + const root = postcss.parse(` + @media {} + @import 'x.css'; + `) + expect(isBlocklessAtRuleAfterBlocklessAtRule(root.nodes[1])).toBe(false) + }) + + it("returns false when non-blockless-at-rule follows a blockless-at-rule", () => { + const root = postcss.parse(` + @import 'y.css'; + @media {} + `) + expect(isBlocklessAtRuleAfterBlocklessAtRule(root.nodes[1])).toBe(false) + }) + + it("returns true when blockless-at-rule follows a blockless-at-rule", () => { + const root = postcss.parse(` + @import 'y.css'; + @import 'x.css'; + `) + expect(isBlocklessAtRuleAfterBlocklessAtRule(root.nodes[1])).toBe(true) + }) + + it("returns true when blockless-at-rule follows a blockless-at-rule with a shared-line comment", () => { + const root = postcss.parse(` + @import 'y.css'; /* comment */ + @import 'x.css'; + `) + expect(isBlocklessAtRuleAfterBlocklessAtRule(root.nodes[2])).toBe(true) + }) +}) diff --git a/lib/utils/__tests__/isBlocklessAtRuleAfterSameNameBlocklessAtRule.test.js b/lib/utils/__tests__/isBlocklessAtRuleAfterSameNameBlocklessAtRule.test.js new file mode 100644 index 0000000000..fc021cc7f8 --- /dev/null +++ b/lib/utils/__tests__/isBlocklessAtRuleAfterSameNameBlocklessAtRule.test.js @@ -0,0 +1,69 @@ +/* @flow */ +"use strict" + +const isBlocklessAtRuleAfterSameNameBlocklessAtRule = require("../isBlocklessAtRuleAfterSameNameBlocklessAtRule") +const postcss = require("postcss") + +describe("isBlocklessAtRuleAfterSameNameBlocklessAtRule", () => { + it("returns false with the first node", () => { + const root = postcss.parse(` + @import 'x.css' + `) + expect(isBlocklessAtRuleAfterSameNameBlocklessAtRule(root.nodes[0])).toBe(false) + }) + + it("returns false with a non-at-rule", () => { + const root = postcss.parse(` + foo {} + `) + expect(isBlocklessAtRuleAfterSameNameBlocklessAtRule(root.nodes[0])).toBe(false) + }) + + it("returns false when blockless-at-rule follows a non-at-rule", () => { + const root = postcss.parse(` + foo {} + @import 'x.css'; + `) + expect(isBlocklessAtRuleAfterSameNameBlocklessAtRule(root.nodes[1])).toBe(false) + }) + + it("returns false when blockless-at-rule follows a non-blockless-at-rule", () => { + const root = postcss.parse(` + @media {} + @import 'x.css'; + `) + expect(isBlocklessAtRuleAfterSameNameBlocklessAtRule(root.nodes[1])).toBe(false) + }) + + it("returns false when non-blockless-at-rule follows a blockless-at-rule", () => { + const root = postcss.parse(` + @import 'y.css'; + @media {} + `) + expect(isBlocklessAtRuleAfterSameNameBlocklessAtRule(root.nodes[1])).toBe(false) + }) + + it("returns false when blockless-at-rule follows a not-same-name-blockless-at-rule", () => { + const root = postcss.parse(` + @extract 'y.css'; + @import 'x.css'; + `) + expect(isBlocklessAtRuleAfterSameNameBlocklessAtRule(root.nodes[1])).toBe(false) + }) + + it("returns true when blockless-at-rule follows a same-name-blockless-at-rule", () => { + const root = postcss.parse(` + @import 'y.css'; + @import 'x.css'; + `) + expect(isBlocklessAtRuleAfterSameNameBlocklessAtRule(root.nodes[1])).toBe(true) + }) + + it("returns true when blockless-at-rule follows a same-name-blockless-at-rule with a shared-line comment", () => { + const root = postcss.parse(` + @import 'y.css'; /* comment */ + @import 'x.css'; + `) + expect(isBlocklessAtRuleAfterSameNameBlocklessAtRule(root.nodes[2])).toBe(true) + }) +}) diff --git a/lib/utils/__tests__/isFirstNestedStatement.test.js b/lib/utils/__tests__/isFirstNestedStatement.test.js new file mode 100644 index 0000000000..f5e7832e6b --- /dev/null +++ b/lib/utils/__tests__/isFirstNestedStatement.test.js @@ -0,0 +1,76 @@ +/* @flow */ +"use strict" + +const isFirstNestedStatement = require("../isFirstNestedStatement") +const postcss = require("postcss") + +describe("isFirstNestedStatement", () => { + it("returns false with the first node", () => { + const root = postcss.parse(` + a { color: 'pink'; } + `) + expect(isFirstNestedStatement(root.nodes[0])).toBe(false) + }) + + it("returns true with the first-nested rule", () => { + const root = postcss.parse(` + @media (min-width: 0px) { + a { color: 'pink'; } + } + `) + + root.walkRules((rule) => { + expect(isFirstNestedStatement(rule)).toBe(true) + }) + }) + + it("returns true with the first-nested at-rule", () => { + const root = postcss.parse(` + a { + @include foo; + } + `) + + root.walkAtRules((atRule) => { + expect(isFirstNestedStatement(atRule)).toBe(true) + }) + }) + + it("returns false with first-nested non-statement", () => { + const root = postcss.parse(` + a { + /* comment */ + } + `) + + root.walkComments((comment) => { + expect(isFirstNestedStatement(comment)).toBe(false) + }) + }) + + it("returns false with not-first-nested rule", () => { + const root = postcss.parse(` + @media (min-width: 0px) { + a { color: 'pink'; } + b { color: 'pink'; } + } + `) + + root.walkRules("b", (rule) => { + expect(isFirstNestedStatement(rule)).toBe(false) + }) + }) + + it("returns false with not-first-nested at-rule", () => { + const root = postcss.parse(` + a { + @include foo; + @expect bar; + } + `) + + root.walkAtRules("expect", (atRule) => { + expect(isFirstNestedStatement(atRule)).toBe(false) + }) + }) +}) diff --git a/lib/utils/__tests__/isSharedLineComment.test.js b/lib/utils/__tests__/isSharedLineComment.test.js new file mode 100644 index 0000000000..8ffa673a91 --- /dev/null +++ b/lib/utils/__tests__/isSharedLineComment.test.js @@ -0,0 +1,113 @@ +/* @flow */ +"use strict" + +const isSharedLineComment = require("../isSharedLineComment") +const postcss = require("postcss") + +describe("isSharedLineComment", () => { + it("returns false for the first node", () => { + const root = postcss.parse(` + /* comment */ + `) + expect(isSharedLineComment(root.nodes[0])).toBe(false) + }) + + it("returns false for a non-shared-line comment before a rule", () => { + const root = postcss.parse(` + /* comment */ + a {} + `) + expect(isSharedLineComment(root.nodes[0])).toBe(false) + }) + + it("returns false for a non-shared-line comment after a rule", () => { + const root = postcss.parse(` + a {} + /* comment */ + `) + expect(isSharedLineComment(root.nodes[1])).toBe(false) + }) + + it("returns true for a shared-line comment at the beginning", () => { + const root = postcss.parse(` + /* comment */ a {} + `) + expect(isSharedLineComment(root.nodes[0])).toBe(true) + }) + + it("returns true for a shared-line comment before a rule", () => { + const root = postcss.parse(` + /* comment */ a {} + `) + expect(isSharedLineComment(root.nodes[0])).toBe(true) + }) + + it("returns true for a shared-line comment after a rule", () => { + const root = postcss.parse(` + a {} /* comment */ + `) + expect(isSharedLineComment(root.nodes[1])).toBe(true) + }) + + it("returns false for a shared-line non-comment", () => { + const root = postcss.parse(` + a {} b {} + `) + expect(isSharedLineComment(root.nodes[0])).toBe(false) + expect(isSharedLineComment(root.nodes[1])).toBe(false) + }) + + it("returns true when comment shares a line with the start of a rule block, before it", () => { + const root = postcss.parse(` + /* comment */ a { + color: pink; + } + `) + expect(isSharedLineComment(root.nodes[0])).toBe(true) + }) + + it("returns true when comment shares a line with the start of a rule block, after it", () => { + const root = postcss.parse(` + a { /* comment */ + color: pink; + } + `) + root.walkComments((comment) => { + expect(isSharedLineComment(comment)).toBe(true) + }) + }) + + it("returns true when comment shares a line with the start of an at-rule block, before it", () => { + const root = postcss.parse(` + /* comment */ @media {@media (min-width: 0px) + a { color: pink; } + } + `) + expect(isSharedLineComment(root.nodes[0])).toBe(true) + }) + + it("returns true when comment shares a line with the start of an at-rule block, after it", () => { + const root = postcss.parse(` + @media (min-width: 0px) { /* comment */ + a { color: pink; } + } + `) + root.walkComments((comment) => { + expect(isSharedLineComment(comment)).toBe(true) + }) + }) + + it("returns false when comment shares a line with only another comment", () => { + const root = postcss.parse(` + /* comment */ /* comment */ + `) + expect(isSharedLineComment(root.nodes[0])).toBe(false) + }) + + it("returns true when comment shares a line with another comment and a non-comment", () => { + const root = postcss.parse(` + /* comment */ /* comment */ a {} + `) + expect(isSharedLineComment(root.nodes[0])).toBe(true) + }) +}) diff --git a/lib/utils/getNextNonSharedLineCommentNode.js b/lib/utils/getNextNonSharedLineCommentNode.js new file mode 100644 index 0000000000..f78580343c --- /dev/null +++ b/lib/utils/getNextNonSharedLineCommentNode.js @@ -0,0 +1,27 @@ +/* @flow */ +"use strict" + +const _ = require("lodash") + +function getNodeLine(node/*:: ?: postcss$node*/)/*: number | void*/ { + return _.get(node, "source.start.line") +} + +module.exports = function getNextNonSharedLineCommentNode(node/*:: ?: postcss$node*/)/*: postcss$node | void*/ { + if (node === undefined) { + return undefined + } + + const nextNode = node.next() + + if (_.get(nextNode, "type") !== "comment") { + return nextNode + } + + if (getNodeLine(node) === getNodeLine(nextNode) + || nextNode !== undefined && getNodeLine(nextNode) === getNodeLine(nextNode.next())) { + return getNextNonSharedLineCommentNode(nextNode) + } + + return nextNode +} diff --git a/lib/utils/getPreviousNonSharedLineCommentNode.js b/lib/utils/getPreviousNonSharedLineCommentNode.js new file mode 100644 index 0000000000..2130375234 --- /dev/null +++ b/lib/utils/getPreviousNonSharedLineCommentNode.js @@ -0,0 +1,29 @@ +/* @flow */ +"use strict" + +const _ = require("lodash") + +function getNodeLine(node/*:: ?: postcss$node*/)/*: number | void*/ { + return _.get(node, "source.start.line") +} + +module.exports = function getPreviousNonSharedLineCommentNode( + node/*:: ?: postcss$node*/ +)/*: postcss$node | void*/ { + if (node === undefined) { + return undefined + } + + const previousNode = node.prev() + + if (_.get(previousNode, "type") !== "comment") { + return previousNode + } + + if (getNodeLine(node) === getNodeLine(previousNode) + || previousNode !== undefined && getNodeLine(previousNode) === getNodeLine(previousNode.prev())) { + return getPreviousNonSharedLineCommentNode(previousNode) + } + + return previousNode +} diff --git a/lib/utils/hasBlock.js b/lib/utils/hasBlock.js index 02350fdcb0..0cc0911639 100644 --- a/lib/utils/hasBlock.js +++ b/lib/utils/hasBlock.js @@ -8,7 +8,7 @@ * @return {boolean} True if `statement` has a block (empty or otherwise) */ module.exports = function ( - statement/*: postcss$rule | postcss$atRule*/ + statement/*: postcss$node*/ )/*: boolean*/ { return statement.nodes !== undefined } diff --git a/lib/utils/isAfterCommentLine.js b/lib/utils/isAfterCommentLine.js new file mode 100644 index 0000000000..787183e56c --- /dev/null +++ b/lib/utils/isAfterCommentLine.js @@ -0,0 +1,14 @@ +/* @flow */ +"use strict" + +const isSharedLineComment = require("./isSharedLineComment") + +module.exports = function (node/*: postcss$node*/)/*: boolean*/ { + const previousNode = node.prev() + + if (!previousNode || previousNode.type !== "comment") { + return false + } + + return !isSharedLineComment(previousNode) +} diff --git a/lib/utils/isAfterStandardPropertyDeclaration.js b/lib/utils/isAfterStandardPropertyDeclaration.js new file mode 100644 index 0000000000..fe2f223fc3 --- /dev/null +++ b/lib/utils/isAfterStandardPropertyDeclaration.js @@ -0,0 +1,16 @@ +/* @flow */ +"use strict" + +const _ = require("lodash") +const getPreviousNonSharedLineCommentNode = require("./getPreviousNonSharedLineCommentNode") +const isStandardSyntaxDeclaration = require("./isStandardSyntaxDeclaration") +const isCustomProperty = require("./isCustomProperty") + +module.exports = function (node/*: postcss$node*/)/*: boolean*/ { + const prevNode = getPreviousNonSharedLineCommentNode(node) + + return prevNode !== undefined + && prevNode.type === "decl" + && isStandardSyntaxDeclaration(prevNode) + && !isCustomProperty(_.get(prevNode, "prop", "")) +} diff --git a/lib/utils/isBlocklessAtRuleAfterBlocklessAtRule.js b/lib/utils/isBlocklessAtRuleAfterBlocklessAtRule.js new file mode 100644 index 0000000000..adc801bf44 --- /dev/null +++ b/lib/utils/isBlocklessAtRuleAfterBlocklessAtRule.js @@ -0,0 +1,27 @@ +/* @flow */ +"use strict" + +const hasBlock = require("./hasBlock") +const isSharedLineComment = require("./isSharedLineComment") + +module.exports = function (atRule/*: postcss$atRule*/)/*: boolean*/ { + if (atRule.type !== "atrule") { + return false + } + + let previousNode = atRule.prev() + if (previousNode === undefined) { + return false + } + + if (isSharedLineComment(previousNode)) { + previousNode = previousNode.prev() + } + if (previousNode === undefined) { + return false + } + + return previousNode.type === "atrule" + && !hasBlock(previousNode) + && !hasBlock(atRule) +} diff --git a/lib/utils/isBlocklessAtRuleAfterSameNameBlocklessAtRule.js b/lib/utils/isBlocklessAtRuleAfterSameNameBlocklessAtRule.js new file mode 100644 index 0000000000..07a8d2c3ea --- /dev/null +++ b/lib/utils/isBlocklessAtRuleAfterSameNameBlocklessAtRule.js @@ -0,0 +1,23 @@ +/* @flow */ +"use strict" + +const _ = require("lodash") +const isBlocklessAtRuleAfterBlocklessAtRule = require("./isBlocklessAtRuleAfterBlocklessAtRule") +const isSharedLineComment = require("./isSharedLineComment") + +module.exports = function (atRule/*: postcss$atRule*/)/*: boolean*/ { + if (!isBlocklessAtRuleAfterBlocklessAtRule(atRule)) { + return false + } + + let previousNode = atRule.prev() + if (previousNode === undefined) { + return false + } + + if (isSharedLineComment(previousNode)) { + previousNode = previousNode.prev() + } + + return _.get(previousNode, "name") == atRule.name +} diff --git a/lib/utils/isFirstNestedStatement.js b/lib/utils/isFirstNestedStatement.js new file mode 100644 index 0000000000..623503f4f6 --- /dev/null +++ b/lib/utils/isFirstNestedStatement.js @@ -0,0 +1,14 @@ +/* @flow */ +"use strict" + +module.exports = function (statement/*: postcss$rule | postcss$atRule*/)/*: boolean*/ { + if (statement.type !== "rule" && statement.type !== "atrule") { + return false + } + + const parentNode = statement.parent + + return parentNode !== undefined + && parentNode.type !== "root" + && statement === parentNode.first +} diff --git a/lib/utils/isSharedLineComment.js b/lib/utils/isSharedLineComment.js new file mode 100644 index 0000000000..4c00d7ec74 --- /dev/null +++ b/lib/utils/isSharedLineComment.js @@ -0,0 +1,35 @@ +/* @flow */ +"use strict" + +const _ = require("lodash") +const getPreviousNonSharedLineCommentNode = require("./getPreviousNonSharedLineCommentNode") +const getNextNonSharedLineCommentNode = require("./getNextNonSharedLineCommentNode") + +function nodesShareLines(a, b) { + return _.get(a, "source.start.line") === _.get(b, "source.start.line") +} + +module.exports = function isSharedLineComment(node/*: postcss$node*/)/*: boolean*/ { + if (node.type !== "comment") { + return false + } + + const previousNonSharedLineCommentNode = getPreviousNonSharedLineCommentNode(node) + if (nodesShareLines(node, previousNonSharedLineCommentNode)) { + return true + } + + const nextNonSharedLineCommentNode = getNextNonSharedLineCommentNode(node) + if (nodesShareLines(node, nextNonSharedLineCommentNode)) { + return true + } + + const parentNode = node.parent + if (parentNode !== undefined + && parentNode.type !== "root" + && parentNode.source.start.line === node.source.start.line) { + return true + } + + return false +}