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.
Files changed (41) hide show
  1. package/dist/oxlint/index.d.ts +6 -0
  2. package/dist/oxlint/index.d.ts.map +1 -0
  3. package/dist/oxlint/index.js +18 -0
  4. package/dist/oxlint/index.js.map +1 -0
  5. package/dist/oxlint/internal/Bindings.d.ts +20 -0
  6. package/dist/oxlint/internal/Bindings.d.ts.map +1 -0
  7. package/dist/oxlint/internal/Bindings.js +203 -0
  8. package/dist/oxlint/internal/Bindings.js.map +1 -0
  9. package/dist/oxlint/internal/Declarations.d.ts +19 -0
  10. package/dist/oxlint/internal/Declarations.d.ts.map +1 -0
  11. package/dist/oxlint/internal/Declarations.js +114 -0
  12. package/dist/oxlint/internal/Declarations.js.map +1 -0
  13. package/dist/oxlint/rules/noConflictingProps.d.ts +5 -0
  14. package/dist/oxlint/rules/noConflictingProps.d.ts.map +1 -0
  15. package/dist/oxlint/rules/noConflictingProps.js +45 -0
  16. package/dist/oxlint/rules/noConflictingProps.js.map +1 -0
  17. package/dist/oxlint/rules/noUnused.d.ts +5 -0
  18. package/dist/oxlint/rules/noUnused.d.ts.map +1 -0
  19. package/dist/oxlint/rules/noUnused.js +62 -0
  20. package/dist/oxlint/rules/noUnused.js.map +1 -0
  21. package/dist/oxlint/rules/restrictedProperties.d.ts +5 -0
  22. package/dist/oxlint/rules/restrictedProperties.d.ts.map +1 -0
  23. package/dist/oxlint/rules/restrictedProperties.js +82 -0
  24. package/dist/oxlint/rules/restrictedProperties.js.map +1 -0
  25. package/dist/oxlint/rules/useLogicalProperties.d.ts +5 -0
  26. package/dist/oxlint/rules/useLogicalProperties.d.ts.map +1 -0
  27. package/dist/oxlint/rules/useLogicalProperties.js +76 -0
  28. package/dist/oxlint/rules/useLogicalProperties.js.map +1 -0
  29. package/dist/oxlint/rules/validStyles.d.ts +5 -0
  30. package/dist/oxlint/rules/validStyles.d.ts.map +1 -0
  31. package/dist/oxlint/rules/validStyles.js +75 -0
  32. package/dist/oxlint/rules/validStyles.js.map +1 -0
  33. package/package.json +7 -1
  34. package/src/oxlint/index.ts +21 -0
  35. package/src/oxlint/internal/Bindings.ts +270 -0
  36. package/src/oxlint/internal/Declarations.ts +137 -0
  37. package/src/oxlint/rules/noConflictingProps.ts +52 -0
  38. package/src/oxlint/rules/noUnused.ts +67 -0
  39. package/src/oxlint/rules/restrictedProperties.ts +92 -0
  40. package/src/oxlint/rules/useLogicalProperties.ts +81 -0
  41. 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
+ }