zyzz 0.0.18 → 0.0.19
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/dist/oxlint/index.d.ts +6 -0
- package/dist/oxlint/index.d.ts.map +1 -0
- package/dist/oxlint/index.js +18 -0
- package/dist/oxlint/index.js.map +1 -0
- package/dist/oxlint/internal/Bindings.d.ts +20 -0
- package/dist/oxlint/internal/Bindings.d.ts.map +1 -0
- package/dist/oxlint/internal/Bindings.js +203 -0
- package/dist/oxlint/internal/Bindings.js.map +1 -0
- package/dist/oxlint/internal/Declarations.d.ts +19 -0
- package/dist/oxlint/internal/Declarations.d.ts.map +1 -0
- package/dist/oxlint/internal/Declarations.js +114 -0
- package/dist/oxlint/internal/Declarations.js.map +1 -0
- package/dist/oxlint/rules/noConflictingProps.d.ts +5 -0
- package/dist/oxlint/rules/noConflictingProps.d.ts.map +1 -0
- package/dist/oxlint/rules/noConflictingProps.js +45 -0
- package/dist/oxlint/rules/noConflictingProps.js.map +1 -0
- package/dist/oxlint/rules/noUnused.d.ts +5 -0
- package/dist/oxlint/rules/noUnused.d.ts.map +1 -0
- package/dist/oxlint/rules/noUnused.js +62 -0
- package/dist/oxlint/rules/noUnused.js.map +1 -0
- package/dist/oxlint/rules/restrictedProperties.d.ts +5 -0
- package/dist/oxlint/rules/restrictedProperties.d.ts.map +1 -0
- package/dist/oxlint/rules/restrictedProperties.js +82 -0
- package/dist/oxlint/rules/restrictedProperties.js.map +1 -0
- package/dist/oxlint/rules/useLogicalProperties.d.ts +5 -0
- package/dist/oxlint/rules/useLogicalProperties.d.ts.map +1 -0
- package/dist/oxlint/rules/useLogicalProperties.js +76 -0
- package/dist/oxlint/rules/useLogicalProperties.js.map +1 -0
- package/dist/oxlint/rules/validStyles.d.ts +5 -0
- package/dist/oxlint/rules/validStyles.d.ts.map +1 -0
- package/dist/oxlint/rules/validStyles.js +75 -0
- package/dist/oxlint/rules/validStyles.js.map +1 -0
- package/package.json +7 -1
- package/src/oxlint/index.ts +21 -0
- package/src/oxlint/internal/Bindings.ts +270 -0
- package/src/oxlint/internal/Declarations.ts +137 -0
- package/src/oxlint/rules/noConflictingProps.ts +52 -0
- package/src/oxlint/rules/noUnused.ts +67 -0
- package/src/oxlint/rules/restrictedProperties.ts +92 -0
- package/src/oxlint/rules/useLogicalProperties.ts +81 -0
- package/src/oxlint/rules/validStyles.ts +88 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Visits declaration positions in styles and recipes without evaluating expressions. @module */
|
|
2
|
+
import type { ESTree } from '@oxlint/plugins'
|
|
3
|
+
import * as Bindings from './Bindings.js'
|
|
4
|
+
import * as Condition from '../../internal/Condition.js'
|
|
5
|
+
|
|
6
|
+
/** Callbacks for known declaration blocks and malformed literal structures. */
|
|
7
|
+
export type Visitors = {
|
|
8
|
+
readonly declaration: (property: ESTree.ObjectProperty, name: string) => void
|
|
9
|
+
readonly invalid?: ((node: ESTree.Node, message: string) => void) | undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Traverses actual style positions, excluding variant selections and callback internals. */
|
|
13
|
+
export function visit(
|
|
14
|
+
node: ESTree.Node,
|
|
15
|
+
options: {
|
|
16
|
+
readonly bindings: ReturnType<typeof Bindings.create>
|
|
17
|
+
readonly recipe: boolean
|
|
18
|
+
readonly visitors: Visitors
|
|
19
|
+
},
|
|
20
|
+
) {
|
|
21
|
+
const { bindings, visitors } = options
|
|
22
|
+
const seen = new Set<ESTree.Node>()
|
|
23
|
+
|
|
24
|
+
function object(
|
|
25
|
+
node: ESTree.Node,
|
|
26
|
+
callback: (node: ESTree.ObjectExpression) => void,
|
|
27
|
+
) {
|
|
28
|
+
node = bindings.expression(node)
|
|
29
|
+
if (seen.has(node)) return
|
|
30
|
+
seen.add(node)
|
|
31
|
+
if (node.type === 'ObjectExpression') callback(node)
|
|
32
|
+
else if (node.type === 'Literal' || node.type === 'ArrayExpression')
|
|
33
|
+
visitors.invalid?.(node, 'Expected a style object.')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function entries(
|
|
37
|
+
node: ESTree.Node,
|
|
38
|
+
callback: (property: ESTree.ObjectProperty) => void,
|
|
39
|
+
) {
|
|
40
|
+
object(node, (node) => {
|
|
41
|
+
for (const property of node.properties) {
|
|
42
|
+
if (property.type === 'SpreadElement') {
|
|
43
|
+
entries(property.argument, callback)
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
if (property.kind !== 'init' || property.method) {
|
|
47
|
+
visitors.invalid?.(
|
|
48
|
+
property,
|
|
49
|
+
'Style objects require data properties, not methods or accessors.',
|
|
50
|
+
)
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
callback(property)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function style(node: ESTree.Node) {
|
|
59
|
+
node = bindings.expression(node)
|
|
60
|
+
if (node.type === 'ArrowFunctionExpression') {
|
|
61
|
+
if (seen.has(node)) return
|
|
62
|
+
seen.add(node)
|
|
63
|
+
if (node.body.type !== 'BlockStatement') style(node.body)
|
|
64
|
+
else
|
|
65
|
+
for (const statement of node.body.body)
|
|
66
|
+
if (statement.type === 'ReturnStatement' && statement.argument)
|
|
67
|
+
style(statement.argument)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
entries(node, (property) => {
|
|
71
|
+
const name = Bindings.key(property)
|
|
72
|
+
if (!name) return
|
|
73
|
+
if (name === 'selectors')
|
|
74
|
+
entries(property.value, (selector) => style(selector.value))
|
|
75
|
+
else if (name === 'targets')
|
|
76
|
+
entries(property.value, (target) => {
|
|
77
|
+
if (Bindings.key(target) === 'web') style(target.value)
|
|
78
|
+
})
|
|
79
|
+
else if (Condition.is(name)) style(property.value)
|
|
80
|
+
else if (name !== 'vars' && name !== 'variables')
|
|
81
|
+
visitors.declaration(property, name)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!options.recipe) style(node)
|
|
86
|
+
else
|
|
87
|
+
entries(node, (property) => {
|
|
88
|
+
const name = Bindings.key(property)
|
|
89
|
+
if (name === 'base') style(property.value)
|
|
90
|
+
else if (name === 'variants')
|
|
91
|
+
entries(property.value, (axis) =>
|
|
92
|
+
entries(axis.value, (choice) => style(choice.value)),
|
|
93
|
+
)
|
|
94
|
+
else if (name === 'compoundVariants') {
|
|
95
|
+
const value = bindings.expression(property.value)
|
|
96
|
+
if (value.type === 'ArrayExpression')
|
|
97
|
+
for (const entry of value.elements)
|
|
98
|
+
if (entry && entry.type !== 'SpreadElement')
|
|
99
|
+
entries(entry, (property) => {
|
|
100
|
+
if (Bindings.key(property) === 'style') style(property.value)
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Extracts known scalar values and fallback arrays without executing user code. */
|
|
107
|
+
export function literal(
|
|
108
|
+
node: ESTree.Node,
|
|
109
|
+
bindings: ReturnType<typeof Bindings.create>,
|
|
110
|
+
seen = new Set<ESTree.Node>(),
|
|
111
|
+
): { readonly value: unknown } | undefined {
|
|
112
|
+
node = bindings.expression(node)
|
|
113
|
+
if (seen.has(node)) return undefined
|
|
114
|
+
seen = new Set(seen).add(node)
|
|
115
|
+
if (node.type === 'Literal') return { value: node.value }
|
|
116
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0)
|
|
117
|
+
return { value: node.quasis[0]?.value.cooked }
|
|
118
|
+
if (
|
|
119
|
+
node.type === 'UnaryExpression' &&
|
|
120
|
+
(node.operator === '-' || node.operator === '+')
|
|
121
|
+
) {
|
|
122
|
+
const operand = literal(node.argument, bindings, seen)
|
|
123
|
+
if (typeof operand?.value === 'number')
|
|
124
|
+
return { value: node.operator === '-' ? -operand.value : operand.value }
|
|
125
|
+
}
|
|
126
|
+
if (node.type === 'ArrayExpression') {
|
|
127
|
+
const values: unknown[] = []
|
|
128
|
+
for (const entry of node.elements) {
|
|
129
|
+
if (!entry || entry.type === 'SpreadElement') return undefined
|
|
130
|
+
const result = literal(entry, bindings, seen)
|
|
131
|
+
if (!result) return undefined
|
|
132
|
+
values.push(result.value)
|
|
133
|
+
}
|
|
134
|
+
return { value: values }
|
|
135
|
+
}
|
|
136
|
+
return undefined
|
|
137
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Finds JSX attributes that overwrite known Zyzz application props. @module */
|
|
2
|
+
import type { ESTree, Rule } from '@oxlint/plugins'
|
|
3
|
+
import * as Bindings from '../internal/Bindings.js'
|
|
4
|
+
|
|
5
|
+
/** Reports competing style spreads and explicit styling attributes without rewriting JSX. */
|
|
6
|
+
export const noConflictingProps: Rule = {
|
|
7
|
+
meta: {
|
|
8
|
+
messages: {
|
|
9
|
+
attribute:
|
|
10
|
+
"Pass '{{property}}' into the Zyzz style call to merge styling props.",
|
|
11
|
+
spread:
|
|
12
|
+
'Compose Zyzz applications with cx() instead of overwriting them with multiple JSX spreads.',
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
type: 'problem',
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
const bindings = Bindings.create(context)
|
|
19
|
+
function application(node: ESTree.Node): boolean {
|
|
20
|
+
node = bindings.expression(node)
|
|
21
|
+
if (node.type !== 'CallExpression') return false
|
|
22
|
+
const binding = bindings.resolve(node.callee)
|
|
23
|
+
return binding?.kind === 'definition' || binding?.kind === 'cx'
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
JSXOpeningElement(node) {
|
|
27
|
+
const spreads = node.attributes.filter(
|
|
28
|
+
(attribute) =>
|
|
29
|
+
attribute.type === 'JSXSpreadAttribute' &&
|
|
30
|
+
application(attribute.argument),
|
|
31
|
+
)
|
|
32
|
+
if (!spreads.length) return
|
|
33
|
+
for (const attribute of node.attributes) {
|
|
34
|
+
if (
|
|
35
|
+
attribute.type !== 'JSXAttribute' ||
|
|
36
|
+
attribute.name.type !== 'JSXIdentifier'
|
|
37
|
+
)
|
|
38
|
+
continue
|
|
39
|
+
const property = attribute.name.name
|
|
40
|
+
if (property === 'className' || property === 'style')
|
|
41
|
+
context.report({
|
|
42
|
+
data: { property },
|
|
43
|
+
messageId: 'attribute',
|
|
44
|
+
node: attribute,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
for (const spread of spreads.slice(1))
|
|
48
|
+
context.report({ messageId: 'spread', node: spread })
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Finds unused style members in local TypeScript namespaces. @module */
|
|
2
|
+
import type { ESTree, Rule } from '@oxlint/plugins'
|
|
3
|
+
import * as Bindings from '../internal/Bindings.js'
|
|
4
|
+
|
|
5
|
+
/** Leaves module exports and escaped namespaces available to unknown consumers. */
|
|
6
|
+
export const noUnused: Rule = {
|
|
7
|
+
meta: {
|
|
8
|
+
messages: { unused: "Style '{{name}}' is never used." },
|
|
9
|
+
schema: [],
|
|
10
|
+
type: 'suggestion',
|
|
11
|
+
},
|
|
12
|
+
create(context) {
|
|
13
|
+
const bindings = Bindings.create(context)
|
|
14
|
+
const declarations: ESTree.VariableDeclarator[] = []
|
|
15
|
+
return {
|
|
16
|
+
VariableDeclarator(node) {
|
|
17
|
+
if (
|
|
18
|
+
node.id.type === 'Identifier' &&
|
|
19
|
+
node.init &&
|
|
20
|
+
bindings.resolve(node.init)?.kind === 'definition'
|
|
21
|
+
)
|
|
22
|
+
declarations.push(node)
|
|
23
|
+
},
|
|
24
|
+
'Program:exit'() {
|
|
25
|
+
for (const declaration of declarations) {
|
|
26
|
+
if (declaration.id.type !== 'Identifier') continue
|
|
27
|
+
let parent: ESTree.Node = declaration.parent
|
|
28
|
+
while (
|
|
29
|
+
parent.type === 'VariableDeclaration' ||
|
|
30
|
+
parent.type === 'ExportNamedDeclaration'
|
|
31
|
+
)
|
|
32
|
+
parent = parent.parent
|
|
33
|
+
if (
|
|
34
|
+
parent.type !== 'TSModuleBlock' ||
|
|
35
|
+
parent.parent.type !== 'TSModuleDeclaration'
|
|
36
|
+
)
|
|
37
|
+
continue
|
|
38
|
+
const namespace = parent.parent
|
|
39
|
+
if (namespace.id.type !== 'Identifier') continue
|
|
40
|
+
// Nested and exported namespaces may have consumers outside this local scope.
|
|
41
|
+
if (namespace.parent.type !== 'Program') continue
|
|
42
|
+
const variable = bindings.variable(declaration.id)
|
|
43
|
+
const owner = bindings.variable(namespace.id)
|
|
44
|
+
if (!variable || !owner || owner.defs.length !== 1) continue
|
|
45
|
+
if (variable.references.some((reference) => reference.isRead()))
|
|
46
|
+
continue
|
|
47
|
+
const used = owner.references.some((reference) => {
|
|
48
|
+
if (!reference.isRead()) return false
|
|
49
|
+
const member = reference.identifier.parent
|
|
50
|
+
return (
|
|
51
|
+
member.type !== 'MemberExpression' ||
|
|
52
|
+
member.object !== reference.identifier ||
|
|
53
|
+
Bindings.key(member) === undefined ||
|
|
54
|
+
Bindings.key(member) === variable.name
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
if (!used)
|
|
58
|
+
context.report({
|
|
59
|
+
data: { name: `${namespace.id.name}.${variable.name}` },
|
|
60
|
+
messageId: 'unused',
|
|
61
|
+
node: declaration.id,
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Applies explicit project restrictions to known web declarations. @module */
|
|
2
|
+
import type { Rule } from '@oxlint/plugins'
|
|
3
|
+
import * as Bindings from '../internal/Bindings.js'
|
|
4
|
+
import * as Declarations from '../internal/Declarations.js'
|
|
5
|
+
|
|
6
|
+
/** Reports forbidden properties or literal values outside an explicit allowlist. */
|
|
7
|
+
export const restrictedProperties: Rule = {
|
|
8
|
+
meta: {
|
|
9
|
+
messages: {
|
|
10
|
+
restricted: "Property '{{property}}' is restricted. {{reason}}",
|
|
11
|
+
},
|
|
12
|
+
schema: [
|
|
13
|
+
{
|
|
14
|
+
additionalProperties: {
|
|
15
|
+
additionalProperties: false,
|
|
16
|
+
properties: {
|
|
17
|
+
reason: { type: 'string' },
|
|
18
|
+
values: { items: { type: ['string', 'number'] }, type: 'array' },
|
|
19
|
+
},
|
|
20
|
+
type: 'object',
|
|
21
|
+
},
|
|
22
|
+
type: 'object',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
type: 'suggestion',
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
const bindings = Bindings.create(context)
|
|
29
|
+
const restrictions = context.options[0]
|
|
30
|
+
const reported = new Set<number>()
|
|
31
|
+
return {
|
|
32
|
+
CallExpression(node) {
|
|
33
|
+
if (
|
|
34
|
+
!restrictions ||
|
|
35
|
+
typeof restrictions !== 'object' ||
|
|
36
|
+
Array.isArray(restrictions)
|
|
37
|
+
)
|
|
38
|
+
return
|
|
39
|
+
const binding = bindings.resolve(node.callee)
|
|
40
|
+
if (binding?.kind !== 'style' && binding?.kind !== 'variants') return
|
|
41
|
+
const input = node.arguments[0]
|
|
42
|
+
if (!input || input.type === 'SpreadElement') return
|
|
43
|
+
Declarations.visit(input, {
|
|
44
|
+
bindings,
|
|
45
|
+
recipe: binding.kind === 'variants',
|
|
46
|
+
visitors: {
|
|
47
|
+
declaration(property, name) {
|
|
48
|
+
if (
|
|
49
|
+
!Object.hasOwn(restrictions, name) ||
|
|
50
|
+
reported.has(property.start)
|
|
51
|
+
)
|
|
52
|
+
return
|
|
53
|
+
const restriction = restrictions[name]
|
|
54
|
+
if (
|
|
55
|
+
!restriction ||
|
|
56
|
+
typeof restriction !== 'object' ||
|
|
57
|
+
Array.isArray(restriction)
|
|
58
|
+
)
|
|
59
|
+
return
|
|
60
|
+
const allowed = restriction.values
|
|
61
|
+
if (Array.isArray(allowed)) {
|
|
62
|
+
const literal = Declarations.literal(property.value, bindings)
|
|
63
|
+
if (!literal) return
|
|
64
|
+
const values: unknown[] = Array.isArray(literal.value)
|
|
65
|
+
? literal.value
|
|
66
|
+
: [literal.value]
|
|
67
|
+
if (
|
|
68
|
+
values.every((value) =>
|
|
69
|
+
allowed.some((allowed) => allowed === value),
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
reported.add(property.start)
|
|
75
|
+
context.report({
|
|
76
|
+
data: {
|
|
77
|
+
property: name,
|
|
78
|
+
reason:
|
|
79
|
+
typeof restriction.reason === 'string'
|
|
80
|
+
? restriction.reason
|
|
81
|
+
: 'Use an approved property or value.',
|
|
82
|
+
},
|
|
83
|
+
messageId: 'restricted',
|
|
84
|
+
node: property.key,
|
|
85
|
+
})
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Encourages logical CSS properties in web style declarations. @module */
|
|
2
|
+
import type { Rule } from '@oxlint/plugins'
|
|
3
|
+
import * as Bindings from '../internal/Bindings.js'
|
|
4
|
+
import * as Declarations from '../internal/Declarations.js'
|
|
5
|
+
|
|
6
|
+
const replacements: Readonly<Record<string, string>> = {
|
|
7
|
+
borderBottomLeftRadius: 'borderEndStartRadius',
|
|
8
|
+
borderBottomRightRadius: 'borderEndEndRadius',
|
|
9
|
+
borderLeft: 'borderInlineStart',
|
|
10
|
+
borderLeftColor: 'borderInlineStartColor',
|
|
11
|
+
borderLeftStyle: 'borderInlineStartStyle',
|
|
12
|
+
borderLeftWidth: 'borderInlineStartWidth',
|
|
13
|
+
borderRight: 'borderInlineEnd',
|
|
14
|
+
borderRightColor: 'borderInlineEndColor',
|
|
15
|
+
borderRightStyle: 'borderInlineEndStyle',
|
|
16
|
+
borderRightWidth: 'borderInlineEndWidth',
|
|
17
|
+
borderTopLeftRadius: 'borderStartStartRadius',
|
|
18
|
+
borderTopRightRadius: 'borderStartEndRadius',
|
|
19
|
+
insetLeft: 'insetInlineStart',
|
|
20
|
+
insetRight: 'insetInlineEnd',
|
|
21
|
+
left: 'insetInlineStart',
|
|
22
|
+
marginLeft: 'marginInlineStart',
|
|
23
|
+
marginRight: 'marginInlineEnd',
|
|
24
|
+
paddingLeft: 'paddingInlineStart',
|
|
25
|
+
paddingRight: 'paddingInlineEnd',
|
|
26
|
+
right: 'insetInlineEnd',
|
|
27
|
+
scrollMarginLeft: 'scrollMarginInlineStart',
|
|
28
|
+
scrollMarginRight: 'scrollMarginInlineEnd',
|
|
29
|
+
scrollPaddingLeft: 'scrollPaddingInlineStart',
|
|
30
|
+
scrollPaddingRight: 'scrollPaddingInlineEnd',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Reports physical properties without changing direction-dependent behavior. */
|
|
34
|
+
export const useLogicalProperties: Rule = {
|
|
35
|
+
meta: {
|
|
36
|
+
messages: {
|
|
37
|
+
logical:
|
|
38
|
+
"Use a CSS logical property instead of '{{property}}', such as '{{replacement}}' for horizontal LTR layouts.",
|
|
39
|
+
},
|
|
40
|
+
schema: [],
|
|
41
|
+
type: 'suggestion',
|
|
42
|
+
},
|
|
43
|
+
create(context) {
|
|
44
|
+
const bindings = Bindings.create(context)
|
|
45
|
+
const reported = new Set<number>()
|
|
46
|
+
return {
|
|
47
|
+
CallExpression(node) {
|
|
48
|
+
const binding = bindings.resolve(node.callee)
|
|
49
|
+
if (binding?.kind !== 'style' && binding?.kind !== 'variants') return
|
|
50
|
+
const input = node.arguments[0]
|
|
51
|
+
if (!input || input.type === 'SpreadElement') return
|
|
52
|
+
Declarations.visit(input, {
|
|
53
|
+
bindings,
|
|
54
|
+
recipe: binding.kind === 'variants',
|
|
55
|
+
visitors: {
|
|
56
|
+
declaration(property, name) {
|
|
57
|
+
const replacement = Object.hasOwn(replacements, name)
|
|
58
|
+
? replacements[name]
|
|
59
|
+
: undefined
|
|
60
|
+
if (!replacement || reported.has(property.start)) return
|
|
61
|
+
if (
|
|
62
|
+
context.sourceCode
|
|
63
|
+
.getCommentsBefore(property)
|
|
64
|
+
.some((comment) =>
|
|
65
|
+
comment.value.includes('allow-physical-property'),
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
return
|
|
69
|
+
reported.add(property.start)
|
|
70
|
+
context.report({
|
|
71
|
+
data: { property: name, replacement },
|
|
72
|
+
messageId: 'logical',
|
|
73
|
+
node: property.key,
|
|
74
|
+
})
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Reports statically known declaration errors using Zyzz's authoring validation. @module */
|
|
2
|
+
import type { ESTree, Rule } from '@oxlint/plugins'
|
|
3
|
+
import * as Bindings from '../internal/Bindings.js'
|
|
4
|
+
import * as Declarations from '../internal/Declarations.js'
|
|
5
|
+
import * as Literal from '../../internal/Literal.js'
|
|
6
|
+
import * as Style from '../../Style.js'
|
|
7
|
+
|
|
8
|
+
/** Validates known structure and values while leaving unresolved bindings to the compiler. */
|
|
9
|
+
export const validStyles: Rule = {
|
|
10
|
+
meta: { messages: { invalid: '{{message}}' }, schema: [], type: 'problem' },
|
|
11
|
+
create(context) {
|
|
12
|
+
const bindings = Bindings.create(context)
|
|
13
|
+
const reported = new Set<number>()
|
|
14
|
+
function report(node: ESTree.Node, message: string) {
|
|
15
|
+
if (reported.has(node.start)) return
|
|
16
|
+
reported.add(node.start)
|
|
17
|
+
context.report({ data: { message }, messageId: 'invalid', node })
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
CallExpression(node) {
|
|
21
|
+
const binding = bindings.resolve(node.callee)
|
|
22
|
+
if (binding?.kind !== 'style' && binding?.kind !== 'variants') return
|
|
23
|
+
const input = node.arguments[0]
|
|
24
|
+
if (!input || input.type === 'SpreadElement') return
|
|
25
|
+
Declarations.visit(input, {
|
|
26
|
+
bindings,
|
|
27
|
+
recipe: binding.kind === 'variants',
|
|
28
|
+
visitors: {
|
|
29
|
+
declaration(property, name) {
|
|
30
|
+
if (binding.themed && name === 'typography') return
|
|
31
|
+
if (
|
|
32
|
+
!binding.themed &&
|
|
33
|
+
!name.startsWith('--') &&
|
|
34
|
+
!Object.hasOwn(Literal.rules, name)
|
|
35
|
+
) {
|
|
36
|
+
report(property.key, `Unknown CSS property '${name}'.`)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
const value = bindings.expression(property.value)
|
|
40
|
+
if (value.type === 'ObjectExpression') {
|
|
41
|
+
report(
|
|
42
|
+
value,
|
|
43
|
+
'CSS declarations require scalar values or fallback arrays.',
|
|
44
|
+
)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
if (
|
|
48
|
+
value.type === 'ArrayExpression' &&
|
|
49
|
+
value.elements.some((element) => !element)
|
|
50
|
+
) {
|
|
51
|
+
report(
|
|
52
|
+
value,
|
|
53
|
+
'Fallback arrays require dense data entries without holes.',
|
|
54
|
+
)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
const literal = Declarations.literal(value, bindings)
|
|
58
|
+
if (!literal) return
|
|
59
|
+
const values = Array.isArray(literal.value)
|
|
60
|
+
? literal.value
|
|
61
|
+
: [literal.value]
|
|
62
|
+
if (
|
|
63
|
+
values.some(
|
|
64
|
+
(value) =>
|
|
65
|
+
typeof value !== 'string' && typeof value !== 'number',
|
|
66
|
+
)
|
|
67
|
+
) {
|
|
68
|
+
report(
|
|
69
|
+
value,
|
|
70
|
+
'CSS declarations require string or number values.',
|
|
71
|
+
)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
Style.define({ lint: { [name]: literal.value } } as never)
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (!(error instanceof Style.InvalidError)) throw error
|
|
78
|
+
for (const diagnostic of error.diagnostics)
|
|
79
|
+
report(value, diagnostic.message)
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
invalid: report,
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
}
|