@hero-design/snowflake-guard 1.0.14 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/lib/src/__tests__/parseSource.spec.js +116 -3
- package/lib/src/parseSource.d.ts +9 -0
- package/lib/src/parseSource.js +8 -0
- package/lib/src/reports/__tests__/reportInlineStyle.spec.d.ts +1 -0
- package/lib/src/reports/__tests__/reportInlineStyle.spec.js +133 -0
- package/lib/src/reports/reportCustomStyleProperties.d.ts +10 -1
- package/lib/src/reports/reportCustomStyleProperties.js +37 -15
- package/lib/src/reports/reportInlineStyle.d.ts +20 -2
- package/lib/src/reports/reportInlineStyle.js +59 -6
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -10,9 +10,18 @@
|
|
|
10
10
|
cp .env.example .env
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
2.
|
|
13
|
+
2. Set up [internal-tool-integrations service](https://github.com/Thinkei/internal-tool-integrations) for the bot to store report.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
- Replace `DB_HOST` with the host of the internal-tool-integrations service.
|
|
16
|
+
- Use the same `SNOWFLAKE_GUARD_SECRET` as the one in the internal-tool-integrations service.
|
|
17
|
+
|
|
18
|
+
In case you don't want to store snowflake report, comment out the `saving report` section in `src/index.ts`.
|
|
19
|
+
|
|
20
|
+
3. Install snowflake-guard[Dev] [app](https://github.com/apps/hero-design-snowflake-guard-dev) to a repository that you wish to run the app on.
|
|
21
|
+
|
|
22
|
+
4. Start `internal-tool-integrations` service.
|
|
23
|
+
|
|
24
|
+
5. Start the bot.
|
|
16
25
|
|
|
17
26
|
```sh
|
|
18
27
|
# Install dependencies
|
|
@@ -21,7 +30,7 @@ yarn install
|
|
|
21
30
|
# Run the bot
|
|
22
31
|
yarn start
|
|
23
32
|
```
|
|
24
|
-
|
|
33
|
+
6. Open or update a pull request to trigger webhook events.
|
|
25
34
|
|
|
26
35
|
## Note
|
|
27
36
|
|
|
@@ -32,11 +32,124 @@ describe('parseSource', () => {
|
|
|
32
32
|
it('reports correct snowflakes', () => {
|
|
33
33
|
const source = fs.readFileSync('./src/__mocks__/sourceSample.tsx', 'utf-8');
|
|
34
34
|
expect((0, parseSource_1.default)(source)).toEqual({
|
|
35
|
+
approvedLocs: [55, 65, 64],
|
|
35
36
|
classNameLocs: [44, 42, 43],
|
|
36
|
-
styleLocs: [54, 47, 49, 50, 51, 52, 53],
|
|
37
|
-
sxLocs: [63, 58, 59, 60, 61, 62],
|
|
37
|
+
styleLocs: [54, 67, 47, 49, 50, 51, 52, 53],
|
|
38
38
|
styledComponentLocs: [6, 10, 15],
|
|
39
|
-
|
|
39
|
+
sxLocs: [63, 58, 59, 60, 61, 62],
|
|
40
|
+
violatingAttributes: [
|
|
41
|
+
{
|
|
42
|
+
attributeName: 'padding',
|
|
43
|
+
attributeValue: '20',
|
|
44
|
+
componentName: 'Button.Link',
|
|
45
|
+
inlineStyleProps: 'style',
|
|
46
|
+
loc: 54,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
attributeName: 'padding',
|
|
50
|
+
attributeValue: '20',
|
|
51
|
+
componentName: 'Button.Link',
|
|
52
|
+
inlineStyleProps: 'sx',
|
|
53
|
+
loc: 63,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
attributeName: 'padding',
|
|
57
|
+
attributeValue: '20',
|
|
58
|
+
componentName: 'Button.Link',
|
|
59
|
+
inlineStyleProps: 'style',
|
|
60
|
+
loc: 67,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
attributeName: 'backgroundColor',
|
|
64
|
+
attributeValue: "'red'",
|
|
65
|
+
componentName: 'Button.Link',
|
|
66
|
+
inlineStyleProps: 'style',
|
|
67
|
+
loc: 67,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
attributeName: 'width',
|
|
71
|
+
attributeValue: '200',
|
|
72
|
+
componentName: 'Empty',
|
|
73
|
+
inlineStyleProps: 'style',
|
|
74
|
+
loc: 47,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
attributeName: 'width',
|
|
78
|
+
attributeValue: '200',
|
|
79
|
+
componentName: 'Card.Header',
|
|
80
|
+
inlineStyleProps: 'style',
|
|
81
|
+
loc: 49,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
attributeName: 'padding',
|
|
85
|
+
attributeValue: '30',
|
|
86
|
+
componentName: 'Button',
|
|
87
|
+
inlineStyleProps: 'style',
|
|
88
|
+
loc: 50,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
attributeName: 'padding',
|
|
92
|
+
attributeValue: '30',
|
|
93
|
+
componentName: 'Button',
|
|
94
|
+
inlineStyleProps: 'style',
|
|
95
|
+
loc: 51,
|
|
96
|
+
additionalProps: [
|
|
97
|
+
{
|
|
98
|
+
propName: 'variant',
|
|
99
|
+
propValue: '"text"',
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
attributeName: 'padding',
|
|
105
|
+
attributeValue: '30',
|
|
106
|
+
componentName: 'Button',
|
|
107
|
+
inlineStyleProps: 'style',
|
|
108
|
+
loc: 52,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
attributeName: 'padding',
|
|
112
|
+
attributeValue: '30',
|
|
113
|
+
componentName: 'Button',
|
|
114
|
+
inlineStyleProps: 'style',
|
|
115
|
+
loc: 53,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
attributeName: 'mt',
|
|
119
|
+
attributeValue: '10',
|
|
120
|
+
componentName: 'Badge.Count',
|
|
121
|
+
inlineStyleProps: 'sx',
|
|
122
|
+
loc: 58,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
attributeName: 'pt',
|
|
126
|
+
attributeValue: '10',
|
|
127
|
+
componentName: 'Badge',
|
|
128
|
+
inlineStyleProps: 'sx',
|
|
129
|
+
loc: 59,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
attributeName: 'pt',
|
|
133
|
+
attributeValue: '10',
|
|
134
|
+
componentName: 'Badge',
|
|
135
|
+
inlineStyleProps: 'sx',
|
|
136
|
+
loc: 60,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
attributeName: 'pt',
|
|
140
|
+
attributeValue: '10',
|
|
141
|
+
componentName: 'Badge',
|
|
142
|
+
inlineStyleProps: 'sx',
|
|
143
|
+
loc: 61,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
attributeName: 'pt',
|
|
147
|
+
attributeValue: '10',
|
|
148
|
+
componentName: 'Badge',
|
|
149
|
+
inlineStyleProps: 'sx',
|
|
150
|
+
loc: 62,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
40
153
|
});
|
|
41
154
|
});
|
|
42
155
|
});
|
package/lib/src/parseSource.d.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import type { CompoundComponentName } from './reports/types';
|
|
2
|
+
import { InlineStyleProps } from './reports/reportInlineStyle';
|
|
1
3
|
declare const parseSource: (source: string) => {
|
|
2
4
|
classNameLocs: number[];
|
|
3
5
|
styleLocs: number[];
|
|
4
6
|
sxLocs: number[];
|
|
5
7
|
styledComponentLocs: number[];
|
|
6
8
|
approvedLocs: number[];
|
|
9
|
+
violatingAttributes: {
|
|
10
|
+
attributeName: string;
|
|
11
|
+
attributeValue: string | null;
|
|
12
|
+
inlineStyleProps: InlineStyleProps;
|
|
13
|
+
componentName: CompoundComponentName;
|
|
14
|
+
loc: number | undefined;
|
|
15
|
+
}[];
|
|
7
16
|
};
|
|
8
17
|
export default parseSource;
|
package/lib/src/parseSource.js
CHANGED
|
@@ -40,6 +40,7 @@ const parseSource = (source) => {
|
|
|
40
40
|
let classNameLocs = [];
|
|
41
41
|
let styleLocs = [];
|
|
42
42
|
let sxLocs = [];
|
|
43
|
+
let violatingAttributes = [];
|
|
43
44
|
const approvedCmtLocs = [];
|
|
44
45
|
const approvedClassnameLocs = [];
|
|
45
46
|
const ast = recast.parse(source, { parser: tsParser });
|
|
@@ -96,6 +97,12 @@ const parseSource = (source) => {
|
|
|
96
97
|
if (hasStyledComponentsImport) {
|
|
97
98
|
styledComponentLocs = (0, reportStyledComponents_1.default)(ast, componentList, styledAliasName).filter(isNotApprovedSnowflakes);
|
|
98
99
|
}
|
|
100
|
+
violatingAttributes = customPropLocs.violatingAttributes.filter((report) => {
|
|
101
|
+
if (report.loc) {
|
|
102
|
+
return !approvedCmtLocs.includes(report.loc);
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
});
|
|
99
106
|
}
|
|
100
107
|
return {
|
|
101
108
|
classNameLocs,
|
|
@@ -103,6 +110,7 @@ const parseSource = (source) => {
|
|
|
103
110
|
sxLocs,
|
|
104
111
|
styledComponentLocs,
|
|
105
112
|
approvedLocs: [...approvedCmtLocs, ...approvedClassnameLocs],
|
|
113
|
+
violatingAttributes,
|
|
106
114
|
};
|
|
107
115
|
};
|
|
108
116
|
exports.default = parseSource;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
const recast = __importStar(require("recast"));
|
|
30
|
+
const reportInlineStyle_1 = __importDefault(require("../reportInlineStyle"));
|
|
31
|
+
const constants_1 = require("../constants");
|
|
32
|
+
const tsParser = __importStar(require("../../parsers/typescript"));
|
|
33
|
+
describe('reportInlineStyle', () => {
|
|
34
|
+
it('should return line numbers and violating attributes for inline styles that violate the ruleset', () => {
|
|
35
|
+
const code = `
|
|
36
|
+
const MyComponent = () => (
|
|
37
|
+
<Card style={{
|
|
38
|
+
color: 'red',
|
|
39
|
+
backgroundColor: 'blue'
|
|
40
|
+
}}
|
|
41
|
+
sx={{
|
|
42
|
+
padding: 10,
|
|
43
|
+
backgroundColor: 'blue'
|
|
44
|
+
}}
|
|
45
|
+
variant="primary"
|
|
46
|
+
intent="success"
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
`;
|
|
50
|
+
// Step 1: Parse the code to generate the AST
|
|
51
|
+
const ast = recast.parse(code, { parser: tsParser });
|
|
52
|
+
const attributes = [];
|
|
53
|
+
// Step 2: Traverse the AST to extract relevant JSXAttributes (style and sx)
|
|
54
|
+
recast.visit(ast, {
|
|
55
|
+
visitJSXAttribute(path) {
|
|
56
|
+
const { node } = path;
|
|
57
|
+
attributes.push(node);
|
|
58
|
+
this.traverse(path);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
const componentName = 'Card';
|
|
62
|
+
// Mock the RULESET_MAP and SX_RULESET_MAP for this test
|
|
63
|
+
constants_1.RULESET_MAP.Card = ['color', 'backgroundColor'];
|
|
64
|
+
constants_1.SX_RULESET_MAP.Card = ['padding', 'backgroundColor'];
|
|
65
|
+
// Step 3: Run the function with the dynamically extracted attributes
|
|
66
|
+
const result = (0, reportInlineStyle_1.default)(ast, attributes, componentName);
|
|
67
|
+
expect(result.additionalProps).toMatchObject([
|
|
68
|
+
{
|
|
69
|
+
propValue: '"primary"',
|
|
70
|
+
propName: 'variant',
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
expect(result.locs.style).toEqual([3]);
|
|
74
|
+
expect(result.locs.sx).toEqual([7]);
|
|
75
|
+
expect(result.violatingAttributes).toEqual([
|
|
76
|
+
{
|
|
77
|
+
attributeName: 'color',
|
|
78
|
+
attributeValue: "'red'",
|
|
79
|
+
componentName: 'Card',
|
|
80
|
+
inlineStyleProps: 'style',
|
|
81
|
+
loc: 3,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
attributeName: 'backgroundColor',
|
|
85
|
+
attributeValue: "'blue'",
|
|
86
|
+
componentName: 'Card',
|
|
87
|
+
inlineStyleProps: 'style',
|
|
88
|
+
loc: 3,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
attributeName: 'padding',
|
|
92
|
+
attributeValue: '10',
|
|
93
|
+
componentName: 'Card',
|
|
94
|
+
inlineStyleProps: 'sx',
|
|
95
|
+
loc: 7,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
attributeName: 'backgroundColor',
|
|
99
|
+
attributeValue: "'blue'",
|
|
100
|
+
componentName: 'Card',
|
|
101
|
+
inlineStyleProps: 'sx',
|
|
102
|
+
loc: 7,
|
|
103
|
+
},
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
it('should return empty arrays when no violations are found', () => {
|
|
107
|
+
const code = `
|
|
108
|
+
const MyComponent = () => (
|
|
109
|
+
<Card style={{ margin: '10px' }} sx={{ margin: 5 }} />
|
|
110
|
+
);
|
|
111
|
+
`;
|
|
112
|
+
// Step 1: Parse the code to generate the AST
|
|
113
|
+
const ast = recast.parse(code, { parser: tsParser });
|
|
114
|
+
const attributes = [];
|
|
115
|
+
// Step 2: Traverse the AST to extract relevant JSXAttributes (style and sx)
|
|
116
|
+
recast.visit(ast, {
|
|
117
|
+
visitJSXAttribute(path) {
|
|
118
|
+
const { node } = path;
|
|
119
|
+
attributes.push(node);
|
|
120
|
+
this.traverse(path);
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
const componentName = 'Card';
|
|
124
|
+
// Mock the RULESET_MAP and SX_RULESET_MAP for this test
|
|
125
|
+
constants_1.RULESET_MAP.Card = ['color'];
|
|
126
|
+
constants_1.SX_RULESET_MAP.Card = ['padding'];
|
|
127
|
+
const result = (0, reportInlineStyle_1.default)(ast, attributes, componentName);
|
|
128
|
+
expect(result.locs.style).toEqual([]);
|
|
129
|
+
expect(result.locs.sx).toEqual([]);
|
|
130
|
+
expect(result.violatingAttributes).toEqual([]);
|
|
131
|
+
expect(result.violatingAttributes).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import * as recast from 'recast';
|
|
2
|
-
import
|
|
2
|
+
import { AdditionalProp, InlineStyleProps } from './reportInlineStyle';
|
|
3
|
+
import type { ComponentName, CompoundComponentName } from './types';
|
|
3
4
|
declare const reportCustomProperties: (ast: recast.types.ASTNode, componentList: {
|
|
4
5
|
[k: string]: "Alert" | "Badge" | "Banner" | "Breadcrumb" | "Button" | "Card" | "Carousel" | "Chart" | "Checkbox" | "Chip" | "Collapse" | "Comment" | "ContextPanel" | "DatePicker" | "Divider" | "Dropdown" | "Empty" | "File" | "Filters" | "Form" | "Grid" | "Icon" | "InPageNavigation" | "Input" | "MediaQuery" | "Menu" | "Modal" | "Notification" | "PageHeader" | "Pagination" | "Portal" | "Portlet" | "Progress" | "Radio" | "Rate" | "Result" | "Select" | "SelectButton" | "SideBar" | "Slider" | "Spinner" | "Statistic" | "Steps" | "Switch" | "Table" | "Tabs" | "Tag" | "TagInput" | "TimePicker" | "Timeline" | "Tooltip" | "Typography" | "Widget";
|
|
5
6
|
}) => {
|
|
6
7
|
className: number[];
|
|
7
8
|
style: number[];
|
|
8
9
|
sx: number[];
|
|
10
|
+
violatingAttributes: {
|
|
11
|
+
attributeName: string;
|
|
12
|
+
attributeValue: string | null;
|
|
13
|
+
inlineStyleProps: InlineStyleProps;
|
|
14
|
+
componentName: CompoundComponentName;
|
|
15
|
+
loc: number | undefined;
|
|
16
|
+
additionalProps?: AdditionalProp[];
|
|
17
|
+
}[];
|
|
9
18
|
};
|
|
10
19
|
export default reportCustomProperties;
|
|
@@ -29,11 +29,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
29
29
|
const recast = __importStar(require("recast"));
|
|
30
30
|
const reportClassName_1 = __importDefault(require("./reportClassName"));
|
|
31
31
|
const reportInlineStyle_1 = __importDefault(require("./reportInlineStyle"));
|
|
32
|
+
const mapViolatingAttributesAndAdditionalProps = (violatingAttributes, additionalProps) => {
|
|
33
|
+
return violatingAttributes.map((violatingAttribute) => (Object.assign(Object.assign({}, violatingAttribute), (additionalProps && additionalProps.length ? { additionalProps } : {}))));
|
|
34
|
+
};
|
|
32
35
|
const reportCustomProperties = (ast, componentList) => {
|
|
33
|
-
const
|
|
36
|
+
const report = {
|
|
34
37
|
className: [],
|
|
35
38
|
style: [],
|
|
36
39
|
sx: [],
|
|
40
|
+
violatingAttributes: [],
|
|
37
41
|
};
|
|
38
42
|
const localComponentList = Object.keys(componentList);
|
|
39
43
|
recast.visit(ast, {
|
|
@@ -44,10 +48,17 @@ const reportCustomProperties = (ast, componentList) => {
|
|
|
44
48
|
localComponentList.includes(path.value.name.name)) {
|
|
45
49
|
const attributes = path.value
|
|
46
50
|
.attributes;
|
|
47
|
-
const styleObjectLocs = (0, reportInlineStyle_1.default)(ast, attributes, componentList[path.value.name.name]);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
const { locs: styleObjectLocs, violatingAttributes, additionalProps, } = (0, reportInlineStyle_1.default)(ast, attributes, componentList[path.value.name.name]);
|
|
52
|
+
report.className = [
|
|
53
|
+
...report.className,
|
|
54
|
+
...(0, reportClassName_1.default)(attributes),
|
|
55
|
+
];
|
|
56
|
+
report.style = [...report.style, ...styleObjectLocs.style];
|
|
57
|
+
report.sx = [...report.sx, ...styleObjectLocs.sx];
|
|
58
|
+
report.violatingAttributes = [
|
|
59
|
+
...report.violatingAttributes,
|
|
60
|
+
...mapViolatingAttributesAndAdditionalProps(violatingAttributes, additionalProps),
|
|
61
|
+
];
|
|
51
62
|
}
|
|
52
63
|
// Case 2: Custom compound component, e.g. <Card.Header />
|
|
53
64
|
if (path.value.name.type === 'JSXMemberExpression' &&
|
|
@@ -58,10 +69,17 @@ const reportCustomProperties = (ast, componentList) => {
|
|
|
58
69
|
].join('.');
|
|
59
70
|
const attributes = path.value
|
|
60
71
|
.attributes;
|
|
61
|
-
const styleObjectLocs = (0, reportInlineStyle_1.default)(ast, attributes, compoundComponentName);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
const { locs: styleObjectLocs, violatingAttributes, additionalProps, } = (0, reportInlineStyle_1.default)(ast, attributes, compoundComponentName);
|
|
73
|
+
report.className = [
|
|
74
|
+
...report.className,
|
|
75
|
+
...(0, reportClassName_1.default)(attributes),
|
|
76
|
+
];
|
|
77
|
+
report.style = [...report.style, ...styleObjectLocs.style];
|
|
78
|
+
report.sx = [...report.sx, ...styleObjectLocs.sx];
|
|
79
|
+
report.violatingAttributes = [
|
|
80
|
+
...report.violatingAttributes,
|
|
81
|
+
...mapViolatingAttributesAndAdditionalProps(violatingAttributes, additionalProps),
|
|
82
|
+
];
|
|
65
83
|
}
|
|
66
84
|
},
|
|
67
85
|
// Case 3: Custom compound component with spead operator. e.g. const { Header } = Card, then <Header />
|
|
@@ -90,13 +108,17 @@ const reportCustomProperties = (ast, componentList) => {
|
|
|
90
108
|
openPath.value.name.name,
|
|
91
109
|
].join('.');
|
|
92
110
|
const { attributes } = openPath.value;
|
|
93
|
-
const styleObjectLocs = (0, reportInlineStyle_1.default)(ast, attributes, compoundComponentName);
|
|
94
|
-
|
|
95
|
-
...
|
|
111
|
+
const { locs: styleObjectLocs, violatingAttributes, additionalProps, } = (0, reportInlineStyle_1.default)(ast, attributes, compoundComponentName);
|
|
112
|
+
report.className = [
|
|
113
|
+
...report.className,
|
|
96
114
|
...(0, reportClassName_1.default)(attributes),
|
|
97
115
|
];
|
|
98
|
-
|
|
99
|
-
|
|
116
|
+
report.style = [...report.style, ...styleObjectLocs.style];
|
|
117
|
+
report.sx = [...report.sx, ...styleObjectLocs.sx];
|
|
118
|
+
report.violatingAttributes = [
|
|
119
|
+
...report.violatingAttributes,
|
|
120
|
+
...mapViolatingAttributesAndAdditionalProps(violatingAttributes, additionalProps),
|
|
121
|
+
];
|
|
100
122
|
}
|
|
101
123
|
},
|
|
102
124
|
});
|
|
@@ -104,6 +126,6 @@ const reportCustomProperties = (ast, componentList) => {
|
|
|
104
126
|
}
|
|
105
127
|
},
|
|
106
128
|
});
|
|
107
|
-
return
|
|
129
|
+
return report;
|
|
108
130
|
};
|
|
109
131
|
exports.default = reportCustomProperties;
|
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import * as recast from 'recast';
|
|
2
2
|
import type { CompoundComponentName } from './types';
|
|
3
|
+
export type InlineStyleProps = 'style' | 'sx';
|
|
4
|
+
export declare const INLINE_STYLE_PROPERTIES: string[];
|
|
5
|
+
export declare const ADDITIONAL_PROPERTIES: string[];
|
|
6
|
+
export type ViolatingAttribute = {
|
|
7
|
+
attributeName: string;
|
|
8
|
+
attributeValue: string | null;
|
|
9
|
+
inlineStyleProps: InlineStyleProps;
|
|
10
|
+
componentName: CompoundComponentName;
|
|
11
|
+
loc: number | undefined;
|
|
12
|
+
};
|
|
13
|
+
export type AdditionalProp = {
|
|
14
|
+
propName: string;
|
|
15
|
+
propValue: string | null;
|
|
16
|
+
};
|
|
3
17
|
declare const reportInlineStyle: (ast: recast.types.ASTNode, attributes: recast.types.namedTypes.JSXAttribute[], componentName: CompoundComponentName) => {
|
|
4
|
-
|
|
5
|
-
|
|
18
|
+
locs: {
|
|
19
|
+
style: number[];
|
|
20
|
+
sx: number[];
|
|
21
|
+
};
|
|
22
|
+
violatingAttributes: ViolatingAttribute[];
|
|
23
|
+
additionalProps: AdditionalProp[];
|
|
6
24
|
};
|
|
7
25
|
export default reportInlineStyle;
|
|
@@ -23,35 +23,80 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.ADDITIONAL_PROPERTIES = exports.INLINE_STYLE_PROPERTIES = void 0;
|
|
26
27
|
const recast = __importStar(require("recast"));
|
|
27
28
|
const constants_1 = require("./constants");
|
|
28
29
|
const BLACKLIST_PROPERTIES = {
|
|
29
30
|
style: constants_1.RULESET_MAP,
|
|
30
31
|
sx: constants_1.SX_RULESET_MAP,
|
|
31
32
|
};
|
|
32
|
-
|
|
33
|
+
exports.INLINE_STYLE_PROPERTIES = ['style', 'sx'];
|
|
34
|
+
exports.ADDITIONAL_PROPERTIES = ['variant']; // Add any additional props you want to track
|
|
35
|
+
const addViolatingAttribute = (prop, styleObjName, componentName, loc, violatingAttributes) => {
|
|
36
|
+
if (prop.key.type !== 'Identifier') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
violatingAttributes.push({
|
|
40
|
+
attributeName: prop.key.name,
|
|
41
|
+
attributeValue: recast.print(prop.value).code,
|
|
42
|
+
inlineStyleProps: styleObjName,
|
|
43
|
+
componentName,
|
|
44
|
+
loc,
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const getPropValue = (attr) => {
|
|
48
|
+
var _a;
|
|
49
|
+
if (!attr.value) {
|
|
50
|
+
return ''; // Handle case where value is missing
|
|
51
|
+
}
|
|
52
|
+
if (typeof attr.value === 'string') {
|
|
53
|
+
return attr.value; // Directly return string value
|
|
54
|
+
}
|
|
55
|
+
if (((_a = attr.value) === null || _a === void 0 ? void 0 : _a.type) === 'JSXExpressionContainer') {
|
|
56
|
+
return recast.print(attr.value.expression).code;
|
|
57
|
+
}
|
|
58
|
+
// Use recast to print the expression if it's not a simple string
|
|
59
|
+
return recast.print(attr.value).code;
|
|
60
|
+
};
|
|
33
61
|
const reportInlineStyle = (ast, attributes, componentName) => {
|
|
34
62
|
const locs = {
|
|
35
63
|
style: [],
|
|
36
64
|
sx: [],
|
|
37
65
|
};
|
|
66
|
+
const violatingAttributes = [];
|
|
67
|
+
const additionalProps = [];
|
|
38
68
|
let hasCustomStyle = false;
|
|
39
69
|
let styleObjName;
|
|
40
70
|
attributes.forEach((attr) => {
|
|
41
71
|
var _a;
|
|
42
|
-
if (attr.type
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
72
|
+
if (attr.type !== 'JSXAttribute') {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (typeof attr.name.name !== 'string') {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (exports.ADDITIONAL_PROPERTIES.includes(attr.name.name)) {
|
|
79
|
+
// Handle expression container like `style={{ ... }}`
|
|
80
|
+
additionalProps.push({
|
|
81
|
+
propName: attr.name.name,
|
|
82
|
+
propValue: getPropValue(attr), // Print expression as string
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (((_a = attr.value) === null || _a === void 0 ? void 0 : _a.type) !== 'JSXExpressionContainer') {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (exports.INLINE_STYLE_PROPERTIES.includes(attr.name.name)) {
|
|
46
89
|
styleObjName = attr.name.name;
|
|
47
90
|
const { expression } = attr.value;
|
|
48
91
|
if (expression.type === 'ObjectExpression') {
|
|
49
92
|
expression.properties.forEach((prop) => {
|
|
93
|
+
var _a;
|
|
50
94
|
// Case 1: Use direct object, e.g. <Card style={{ color: 'red' }} />
|
|
51
95
|
if (prop.type === 'ObjectProperty' &&
|
|
52
96
|
prop.key.type === 'Identifier' &&
|
|
53
97
|
BLACKLIST_PROPERTIES[styleObjName][componentName].includes(prop.key.name)) {
|
|
54
98
|
hasCustomStyle = true;
|
|
99
|
+
addViolatingAttribute(prop, styleObjName, componentName, (_a = attr.loc) === null || _a === void 0 ? void 0 : _a.start.line, violatingAttributes);
|
|
55
100
|
}
|
|
56
101
|
// Case 2: Use spread operator, e.g. <Card style={{ ...customStyle }} />
|
|
57
102
|
if (prop.type === 'SpreadElement' &&
|
|
@@ -67,10 +112,12 @@ const reportInlineStyle = (ast, attributes, componentName) => {
|
|
|
67
112
|
declaration.id.name === variableName &&
|
|
68
113
|
((_a = declaration.init) === null || _a === void 0 ? void 0 : _a.type) === 'ObjectExpression') {
|
|
69
114
|
declaration.init.properties.forEach((deProp) => {
|
|
115
|
+
var _a;
|
|
70
116
|
if (deProp.type === 'ObjectProperty' &&
|
|
71
117
|
deProp.key.type === 'Identifier' &&
|
|
72
118
|
BLACKLIST_PROPERTIES[styleObjName][componentName].includes(deProp.key.name)) {
|
|
73
119
|
hasCustomStyle = true;
|
|
120
|
+
addViolatingAttribute(deProp, styleObjName, componentName, (_a = attr.loc) === null || _a === void 0 ? void 0 : _a.start.line, violatingAttributes);
|
|
74
121
|
}
|
|
75
122
|
});
|
|
76
123
|
}
|
|
@@ -99,10 +146,12 @@ const reportInlineStyle = (ast, attributes, componentName) => {
|
|
|
99
146
|
deProp.key.name === propName &&
|
|
100
147
|
deProp.value.type === 'ObjectExpression') {
|
|
101
148
|
deProp.value.properties.forEach((p) => {
|
|
149
|
+
var _a;
|
|
102
150
|
if (p.type === 'ObjectProperty' &&
|
|
103
151
|
p.key.type === 'Identifier' &&
|
|
104
152
|
BLACKLIST_PROPERTIES[styleObjName][componentName].includes(p.key.name)) {
|
|
105
153
|
hasCustomStyle = true;
|
|
154
|
+
addViolatingAttribute(p, styleObjName, componentName, (_a = attr.loc) === null || _a === void 0 ? void 0 : _a.start.line, violatingAttributes);
|
|
106
155
|
}
|
|
107
156
|
});
|
|
108
157
|
}
|
|
@@ -126,10 +175,12 @@ const reportInlineStyle = (ast, attributes, componentName) => {
|
|
|
126
175
|
declaration.id.name === variableName &&
|
|
127
176
|
((_a = declaration.init) === null || _a === void 0 ? void 0 : _a.type) === 'ObjectExpression') {
|
|
128
177
|
declaration.init.properties.forEach((prop) => {
|
|
178
|
+
var _a;
|
|
129
179
|
if (prop.type === 'ObjectProperty' &&
|
|
130
180
|
prop.key.type === 'Identifier' &&
|
|
131
181
|
BLACKLIST_PROPERTIES[styleObjName][componentName].includes(prop.key.name)) {
|
|
132
182
|
hasCustomStyle = true;
|
|
183
|
+
addViolatingAttribute(prop, styleObjName, componentName, (_a = attr.loc) === null || _a === void 0 ? void 0 : _a.start.line, violatingAttributes);
|
|
133
184
|
}
|
|
134
185
|
});
|
|
135
186
|
}
|
|
@@ -157,10 +208,12 @@ const reportInlineStyle = (ast, attributes, componentName) => {
|
|
|
157
208
|
prop.key.name === propName &&
|
|
158
209
|
prop.value.type === 'ObjectExpression') {
|
|
159
210
|
prop.value.properties.forEach((p) => {
|
|
211
|
+
var _a;
|
|
160
212
|
if (p.type === 'ObjectProperty' &&
|
|
161
213
|
p.key.type === 'Identifier' &&
|
|
162
214
|
BLACKLIST_PROPERTIES[styleObjName][componentName].includes(p.key.name)) {
|
|
163
215
|
hasCustomStyle = true;
|
|
216
|
+
addViolatingAttribute(p, styleObjName, componentName, (_a = attr.loc) === null || _a === void 0 ? void 0 : _a.start.line, violatingAttributes);
|
|
164
217
|
}
|
|
165
218
|
});
|
|
166
219
|
}
|
|
@@ -174,6 +227,6 @@ const reportInlineStyle = (ast, attributes, componentName) => {
|
|
|
174
227
|
}
|
|
175
228
|
}
|
|
176
229
|
});
|
|
177
|
-
return locs;
|
|
230
|
+
return { locs, violatingAttributes, additionalProps };
|
|
178
231
|
};
|
|
179
232
|
exports.default = reportInlineStyle;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hero-design/snowflake-guard",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A hero-design bot detecting snowflake usage",
|
|
5
5
|
"author": "Hau Dao",
|
|
6
6
|
"license": "ISC",
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc",
|
|
20
20
|
"start": "yarn build && probot run ./lib/src/index.js",
|
|
21
|
+
"type-check": "tsc --noEmit",
|
|
21
22
|
"test": "jest --passWithNoTests",
|
|
23
|
+
"lint": "eslint src",
|
|
22
24
|
"deploy": "netlify deploy --site snowflake-guard.netlify.app --prod --auth $NETLIFY_AUTH_TOKEN",
|
|
23
25
|
"publish:npm": "yarn publish --access public"
|
|
24
26
|
},
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
"@types/jest": "^29.0.0",
|
|
35
37
|
"@types/node": "^18.0.0",
|
|
36
38
|
"config-tsconfig": "8.42.4",
|
|
39
|
+
"eslint": "^8.56.0",
|
|
37
40
|
"eslint-config-hd": "8.42.4",
|
|
38
41
|
"jest": "^29.0.0",
|
|
39
42
|
"nock": "^13.0.5",
|