@omnifyjp/ts 6.7.16 → 6.7.17
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.
|
@@ -462,6 +462,7 @@ function generateUserModel(name, schema, reader, config, hasNestedSet = false) {
|
|
|
462
462
|
fillable: fillableFieldNames(properties, expandedProperties, propertyOrder),
|
|
463
463
|
hidden: hiddenFieldNames(properties, expandedProperties, propertyOrder),
|
|
464
464
|
appends: appendsFieldNames(expandedProperties),
|
|
465
|
+
casts: castsFieldNames(properties, expandedProperties, propertyOrder, reader, config),
|
|
465
466
|
};
|
|
466
467
|
const content = `<?php
|
|
467
468
|
|
|
@@ -837,6 +838,17 @@ function buildCasts(properties, expandedProperties, propertyOrder, reader, confi
|
|
|
837
838
|
return '';
|
|
838
839
|
return casts.map(c => ` ${c}`).join('\n') + '\n';
|
|
839
840
|
}
|
|
841
|
+
function castsFieldNames(properties, expandedProperties, propertyOrder, reader, config) {
|
|
842
|
+
const body = buildCasts(properties, expandedProperties, propertyOrder, reader, config);
|
|
843
|
+
if (body === '') {
|
|
844
|
+
return [];
|
|
845
|
+
}
|
|
846
|
+
const keys = [];
|
|
847
|
+
for (const m of body.matchAll(/'((?:[^'\\]|\\.)*)'\s*=>/g)) {
|
|
848
|
+
keys.push(m[1].replace(/\\'/g, "'"));
|
|
849
|
+
}
|
|
850
|
+
return keys;
|
|
851
|
+
}
|
|
840
852
|
/**
|
|
841
853
|
* Issue #98 v5.8.10: resolve the FQCN namespace of a related Model for
|
|
842
854
|
* inline `belongsTo / hasMany / belongsToMany / morphTo` references.
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type ShadowingEloquentProperty, type ShadowingPropertyConflict } from './types.js';
|
|
2
2
|
/** Case-sensitive intent marker checked against raw file content (issue #222). */
|
|
3
3
|
export declare function hasIntentionalShadowMarker(rawContent: string, prop: string): boolean;
|
|
4
|
+
/** Quoted array keys before `=>` in a PHP array literal segment (casts keys only). */
|
|
5
|
+
export declare function parseCastAssociativeArrayKeys(segment: string): string[];
|
|
6
|
+
export declare function parseRedeclaredCastsPropertyKeys(source: string): string[] | null;
|
|
7
|
+
export declare function parseRedeclaredCastsMethodKeys(source: string): string[] | null;
|
|
4
8
|
/**
|
|
5
9
|
* Build the `conflicts` entries for a generated model's editable subclass.
|
|
6
10
|
*/
|
|
@@ -17,7 +21,4 @@ export type ShadowingConflictResult = {
|
|
|
17
21
|
message: string;
|
|
18
22
|
drift: ShadowingDrift;
|
|
19
23
|
};
|
|
20
|
-
/**
|
|
21
|
-
* Evaluate shadowing conflicts for a skipped user-editable model file.
|
|
22
|
-
*/
|
|
23
24
|
export declare function evaluateShadowingConflicts(existingRaw: string, conflicts: readonly ShadowingPropertyConflict[]): ShadowingConflictResult[];
|
|
@@ -20,10 +20,66 @@ function shadowingConsequence(prop) {
|
|
|
20
20
|
case 'appends':
|
|
21
21
|
return (`Accessors omnify adds to $appends from now on would not be appended to ` +
|
|
22
22
|
`the model's array or JSON form.`);
|
|
23
|
+
case 'casts':
|
|
24
|
+
return (`Casts omnify adds from now on are lost: a Json column without its cast throws ` +
|
|
25
|
+
`"Array to string conversion" at INSERT, and a Decimal column silently returns a ` +
|
|
26
|
+
`differently-formatted string per driver.`);
|
|
23
27
|
default:
|
|
24
28
|
return `Configuration omnify adds to $${prop} from now on stops applying on this model.`;
|
|
25
29
|
}
|
|
26
30
|
}
|
|
31
|
+
const CASTS_METHOD_PATTERN = String.raw `(?:public|protected)\s+function\s+casts\s*\(`;
|
|
32
|
+
function shadowingCastsMethodHint(baseClass) {
|
|
33
|
+
return (`redeclares casts(), which REPLACES ${baseClass}::casts() rather than merging with it ` +
|
|
34
|
+
`(PHP does not merge methods). Casts omnify adds from now on are lost: a Json column ` +
|
|
35
|
+
`without its cast throws "Array to string conversion" at INSERT, and a Decimal column ` +
|
|
36
|
+
`silently returns a differently-formatted string per driver. ` +
|
|
37
|
+
`Fix: return array_merge(parent::casts(), [ … ]). ` +
|
|
38
|
+
`If intentional, add \`/** @omnify-casts intentional */\` above the method to silence ` +
|
|
39
|
+
`this warning.`);
|
|
40
|
+
}
|
|
41
|
+
/** Quoted array keys before `=>` in a PHP array literal segment (casts keys only). */
|
|
42
|
+
export function parseCastAssociativeArrayKeys(segment) {
|
|
43
|
+
const keys = [];
|
|
44
|
+
const singleQuoted = /'((?:[^'\\]|\\.)*)'\s*=>/g;
|
|
45
|
+
for (const m of segment.matchAll(singleQuoted)) {
|
|
46
|
+
keys.push(m[1].replace(/\\'/g, "'"));
|
|
47
|
+
}
|
|
48
|
+
const doubleQuoted = /"((?:[^"\\]|\\.)*)"\s*=>/g;
|
|
49
|
+
for (const m of segment.matchAll(doubleQuoted)) {
|
|
50
|
+
keys.push(m[1].replace(/\\"/g, '"'));
|
|
51
|
+
}
|
|
52
|
+
return keys;
|
|
53
|
+
}
|
|
54
|
+
export function parseRedeclaredCastsPropertyKeys(source) {
|
|
55
|
+
const stripped = stripPhpComments(source);
|
|
56
|
+
const match = stripped.match(/\$casts\s*=\s*\[[\s\S]*?\];/);
|
|
57
|
+
if (!match) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return parseCastAssociativeArrayKeys(match[0]);
|
|
61
|
+
}
|
|
62
|
+
export function parseRedeclaredCastsMethodKeys(source) {
|
|
63
|
+
const stripped = stripPhpComments(source);
|
|
64
|
+
const fnIdx = stripped.search(/function\s+casts\s*\(/);
|
|
65
|
+
if (fnIdx < 0) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const tail = stripped.slice(fnIdx);
|
|
69
|
+
const returnMatch = tail.match(/return\s*\[[\s\S]*?\];/);
|
|
70
|
+
if (!returnMatch) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return parseCastAssociativeArrayKeys(returnMatch[0]);
|
|
74
|
+
}
|
|
75
|
+
function redeclaredCastsMethodBody(source) {
|
|
76
|
+
const stripped = stripPhpComments(source);
|
|
77
|
+
const match = stripped.match(new RegExp(CASTS_METHOD_PATTERN + String.raw `[\s\S]*?\{([\s\S]*?)\}`));
|
|
78
|
+
return match ? match[1] : null;
|
|
79
|
+
}
|
|
80
|
+
function childDeclaresCastsMethod(stripped) {
|
|
81
|
+
return new RegExp(CASTS_METHOD_PATTERN).test(stripped);
|
|
82
|
+
}
|
|
27
83
|
function shadowingFixAdvice(prop, baseClass) {
|
|
28
84
|
return (`If this narrowing is intentional, add \`/** @omnify-${prop} intentional */\` ` +
|
|
29
85
|
`above the property to silence this warning. If not, remove the redeclaration ` +
|
|
@@ -33,13 +89,20 @@ function shadowingFixAdvice(prop, baseClass) {
|
|
|
33
89
|
* Build the `conflicts` entries for a generated model's editable subclass.
|
|
34
90
|
*/
|
|
35
91
|
export function shadowingPropertyConflicts(baseClass, baseArrayValues) {
|
|
36
|
-
|
|
92
|
+
const castsBaseValues = baseArrayValues?.casts;
|
|
93
|
+
const castsKeys = castsBaseValues &&
|
|
94
|
+
castsBaseValues.length > 0 &&
|
|
95
|
+
castsBaseValues.every((v) => typeof v === 'string')
|
|
96
|
+
? castsBaseValues
|
|
97
|
+
: undefined;
|
|
98
|
+
const propertyConflicts = SHADOWING_ELOQUENT_PROPERTIES.map((prop) => {
|
|
37
99
|
const values = baseArrayValues?.[prop];
|
|
38
100
|
const baseValues = values && values.length > 0 && values.every((v) => typeof v === 'string')
|
|
39
101
|
? values
|
|
40
102
|
: undefined;
|
|
41
103
|
return {
|
|
42
104
|
property: prop,
|
|
105
|
+
form: 'property',
|
|
43
106
|
pattern: String.raw `(?:public|protected|private)\s+(?:static\s+)?(?:array\s+)?\$` +
|
|
44
107
|
prop +
|
|
45
108
|
String.raw `\s*=`,
|
|
@@ -49,6 +112,14 @@ export function shadowingPropertyConflicts(baseClass, baseArrayValues) {
|
|
|
49
112
|
...(baseValues ? { baseValues } : {}),
|
|
50
113
|
};
|
|
51
114
|
});
|
|
115
|
+
const methodCastsConflict = {
|
|
116
|
+
property: 'casts',
|
|
117
|
+
form: 'method',
|
|
118
|
+
pattern: CASTS_METHOD_PATTERN,
|
|
119
|
+
hint: shadowingCastsMethodHint(baseClass),
|
|
120
|
+
...(castsKeys ? { baseValues: castsKeys } : {}),
|
|
121
|
+
};
|
|
122
|
+
return [...propertyConflicts, methodCastsConflict];
|
|
52
123
|
}
|
|
53
124
|
/**
|
|
54
125
|
* Parse a redeclared PHP string array property (first `];` closes the array).
|
|
@@ -114,21 +185,41 @@ function classifyShadowingDrift(baseValues, childValues) {
|
|
|
114
185
|
/**
|
|
115
186
|
* Evaluate shadowing conflicts for a skipped user-editable model file.
|
|
116
187
|
*/
|
|
188
|
+
function childValuesForConflict(existingRaw, conflict, baseValues) {
|
|
189
|
+
if (!baseValues) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (conflict.property === 'casts') {
|
|
193
|
+
if (conflict.form === 'method') {
|
|
194
|
+
return parseRedeclaredCastsMethodKeys(existingRaw);
|
|
195
|
+
}
|
|
196
|
+
return parseRedeclaredCastsPropertyKeys(existingRaw);
|
|
197
|
+
}
|
|
198
|
+
return parseRedeclaredStringArray(existingRaw, conflict.property);
|
|
199
|
+
}
|
|
117
200
|
export function evaluateShadowingConflicts(existingRaw, conflicts) {
|
|
118
201
|
const stripped = stripPhpComments(existingRaw);
|
|
119
202
|
const results = [];
|
|
203
|
+
const skipsPropertyCasts = childDeclaresCastsMethod(stripped);
|
|
120
204
|
for (const conflict of conflicts) {
|
|
205
|
+
if (conflict.property === 'casts' && conflict.form !== 'method' && skipsPropertyCasts) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
121
208
|
if (hasIntentionalShadowMarker(existingRaw, conflict.property)) {
|
|
122
209
|
continue;
|
|
123
210
|
}
|
|
124
211
|
if (!new RegExp(conflict.pattern).test(stripped)) {
|
|
125
212
|
continue;
|
|
126
213
|
}
|
|
214
|
+
if (conflict.form === 'method' && conflict.property === 'casts') {
|
|
215
|
+
const body = redeclaredCastsMethodBody(existingRaw);
|
|
216
|
+
if (body !== null && body.includes('parent::casts()')) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
127
220
|
let message = conflict.hint;
|
|
128
221
|
const baseValues = conflict.baseValues && conflict.baseValues.length > 0 ? conflict.baseValues : undefined;
|
|
129
|
-
const childValues = baseValues
|
|
130
|
-
? parseRedeclaredStringArray(existingRaw, conflict.property)
|
|
131
|
-
: null;
|
|
222
|
+
const childValues = childValuesForConflict(existingRaw, conflict, baseValues);
|
|
132
223
|
const drift = classifyShadowingDrift(baseValues, childValues);
|
|
133
224
|
if (baseValues) {
|
|
134
225
|
const driftText = formatShadowingDrift(baseValues, childValues);
|
package/dist/php/types.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export type ShadowingPropertyConflict = {
|
|
|
56
56
|
readonly pattern: string;
|
|
57
57
|
readonly hint: string;
|
|
58
58
|
readonly baseValues?: readonly string[];
|
|
59
|
+
/** Property redeclaration vs method redeclaration (issue #245: casts()). */
|
|
60
|
+
readonly form?: 'property' | 'method';
|
|
59
61
|
};
|
|
60
62
|
/** Category of base file for modular path resolution. */
|
|
61
63
|
export type BaseCategory = 'Models' | 'Controllers' | 'Requests' | 'Resources' | 'Services' | 'Enums' | 'Traits' | 'Locales' | 'Policies';
|