Scheduler — Add ESLint naming-convention rule to forbid underscore-prefixed members#33126
Scheduler — Add ESLint naming-convention rule to forbid underscore-prefixed members#33126aleksei-semikozov wants to merge 18 commits intoDevExpress:26_1from
Conversation
2b90428 to
d3a8e3c
Compare
There was a problem hiding this comment.
Pull request overview
This PR tightens linting for the internal Scheduler TypeScript code by forbidding underscore-prefixed member names, while allowing a curated set of legacy/override members to keep existing code compliant.
Changes:
- Add a Scheduler-specific allowlist and regex to exempt known legacy/override underscore members from the naming-convention rule.
- Change
@typescript-eslint/naming-conventionfor Scheduler to forbid underscore-prefixedmemberLikeidentifiers by default. - Add a temporary override to still allow underscore-prefixed members under
js/__internal/scheduler/appointments/**pending refactoring.
| const schedulerMemberAllowlistRegex = | ||
| `^(_|__esModule|${schedulerMemberAllowlist.map(s => s.replace(/\$/g, '\\$')).join('|')})$`; |
There was a problem hiding this comment.
schedulerMemberAllowlistRegex only escapes $ in allowlisted member names. That makes the regex fragile: if any current/future allowlist entry contains other regex metacharacters (e.g. ., +, ?, [), the filter will misbehave and may accidentally allow/forbid the wrong identifiers. Consider using a small helper to escape all regex metacharacters for each allowlisted string before joining them into the alternation.
| const schedulerMemberAllowlistRegex = | |
| `^(_|__esModule|${schedulerMemberAllowlist.map(s => s.replace(/\$/g, '\\$')).join('|')})$`; | |
| function escapeRegex(str) { | |
| return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| } | |
| const schedulerMemberAllowlistRegex = | |
| `^(_|__esModule|${schedulerMemberAllowlist.map(escapeRegex).join('|')})$`; |
| '@typescript-eslint/naming-convention': [ | ||
| 'error', | ||
| { | ||
| selector: ['variable', 'function', 'parameter'], | ||
| format: null, | ||
| leadingUnderscore: 'forbid', | ||
| filter: { | ||
| regex: '^_$', | ||
| match: false, | ||
| }, | ||
| }, | ||
| { | ||
| selector: 'memberLike', | ||
| format: null, | ||
| leadingUnderscore: 'allow', | ||
| }, | ||
| ], |
There was a problem hiding this comment.
The @typescript-eslint/naming-convention configuration for scheduler is duplicated here just to change memberLike.leadingUnderscore behavior. To avoid the two blocks drifting over time, consider extracting the shared naming-convention options (e.g., the variable/function/parameter selector config) into a reusable constant and reusing it in both places.
|
|
||
| export class ViewDataGeneratorDay extends ViewDataGenerator { | ||
| _calculateStartViewDate(options) { | ||
| calculateStartViewDate(options) { |
There was a problem hiding this comment.
calculateStartViewDate overrides a protected method on ViewDataGenerator, but without an explicit modifier it becomes public here. Mark it as protected to avoid accidentally expanding the public API surface and to keep access levels consistent across the hierarchy.
| calculateStartViewDate(options) { | |
| protected calculateStartViewDate(options) { |
| } | ||
|
|
||
| _calculateStartViewDate(options) { | ||
| calculateStartViewDate(options) { |
There was a problem hiding this comment.
calculateStartViewDate is protected in the base ViewDataGenerator, but this override is implicitly public. Please add the protected modifier to keep the same accessibility as the base method.
| calculateStartViewDate(options) { | |
| protected calculateStartViewDate(options) { |
| } | ||
|
|
||
| _calculateStartViewDate(options) { | ||
| calculateStartViewDate(options) { |
There was a problem hiding this comment.
calculateStartViewDate is protected in the base class, but this override is implicitly public. Add protected to keep the intended visibility and avoid widening the API surface.
| calculateStartViewDate(options) { | |
| protected calculateStartViewDate(options) { |
| } | ||
|
|
||
| _calculateStartViewDate(options: any) { | ||
| calculateStartViewDate(options: any) { |
There was a problem hiding this comment.
calculateStartViewDate overrides a protected base method, but is implicitly public here. Consider adding the protected modifier to keep access levels consistent.
| calculateStartViewDate(options: any) { | |
| protected calculateStartViewDate(options: any) { |
| } | ||
|
|
||
| _calculateStartViewDate(options) { | ||
| calculateStartViewDate(options) { |
There was a problem hiding this comment.
calculateStartViewDate overrides a protected method from ViewDataGenerator, but is implicitly public here. Add protected to avoid exposing it as a public method.
| calculateStartViewDate(options) { | |
| protected calculateStartViewDate(options) { |
| } | ||
|
|
||
| _setVisibilityDates(options) { | ||
| setVisibilityDates(options) { |
There was a problem hiding this comment.
setVisibilityDates is protected in ViewDataGenerator, but this override is implicitly public. Mark it as protected to keep the same accessibility as the base method.
| setVisibilityDates(options) { | |
| protected setVisibilityDates(options) { |
| protected getAllDayHeight(showAllDayPanel) { | ||
| return getAllDayHeight(showAllDayPanel, true, this.DOMMetaData); | ||
| } |
There was a problem hiding this comment.
This new getAllDayHeight method has the same name as the module-level getAllDayHeight helper, which makes the implementation read like a recursive call. Consider aliasing the helper (e.g., getAllDayHeight as calculateAllDayHeight) or renaming the method to avoid the name collision.
| export const schedulerMemberAllowlistRegex = | ||
| `^(_|__esModule|${schedulerMemberAllowlist.map(s => s.replace(/\$/g, '\\$')).join('|')})$`; |
There was a problem hiding this comment.
The $ escaping in schedulerMemberAllowlistRegex looks incorrect: s.replace(/\$/g, '\\$') produces \\$ in the final regex source, which will be parsed by RegExp as a literal backslash followed by an end-of-string anchor (so allowlisted members like _$dateTable won’t actually match the filter). This will cause @typescript-eslint/naming-convention to report underscore-prefixed DOM members that contain $.
Use a single regex-escape for $ in the generated pattern (i.e., ensure the final pattern contains \$, not \\$), or switch to a general regex-escape helper for all special characters.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
packages/devextreme/js/__internal/scheduler/workspaces/helpers/m_position_helper.ts:243
getMaxAllowedVerticalPositionappears to be an internal helper (previously underscore-prefixed) and is only used inside these strategy classes. Consider making itprotected(orprivate) to preserve encapsulation and prevent other code from depending on it as a public method.
getMaxAllowedVerticalPosition({
groupIndex,
showAllDayPanel,
isGroupedAllDayPanel,
}) {
const { rowIndex } = this.viewDataProvider.getLastGroupCellPosition(groupIndex);
| } | ||
|
|
||
| _calculateCellIndex(rowIndex, columnIndex, rowCount, columnCount) { | ||
| calculateCellIndex(rowIndex, columnIndex, rowCount, columnCount) { |
There was a problem hiding this comment.
calculateCellIndex overrides a protected method in ViewDataGenerator, but is currently public (no modifier). Mark it protected to match the base class visibility and avoid widening the API surface.
| calculateCellIndex(rowIndex, columnIndex, rowCount, columnCount) { | |
| protected calculateCellIndex(rowIndex, columnIndex, rowCount, columnCount) { |
| export const schedulerMemberAllowlistRegex = | ||
| `^(_|__esModule|${schedulerMemberAllowlist.map(s => s.replace(/\$/g, '\\$')).join('|')})$`; |
There was a problem hiding this comment.
The allowlist regex escapes only $. If any future allowlisted member name includes other regex meta-characters (e.g. ., ?, +, |, (), this will break the regex or change its meaning. Consider escaping all regex-special characters when building the pattern (or using a dedicated escape helper) to make this list safer to maintain.
| export const schedulerMemberAllowlistRegex = | |
| `^(_|__esModule|${schedulerMemberAllowlist.map(s => s.replace(/\$/g, '\\$')).join('|')})$`; | |
| const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| export const schedulerMemberAllowlistRegex = | |
| `^(_|__esModule|${schedulerMemberAllowlist.map(s => escapeRegex(s)).join('|')})$`; |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 23 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (3)
packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts:25
calculateStartViewDateis a protected hook inViewDataGenerator, but this override is currently public (no access modifier). After dropping the underscore prefix, please consider marking itprotectedto keep the method as an internal extension point rather than a public API.
calculateStartViewDate(options: any) {
return timelineMonthUtils.calculateStartViewDate(
options.currentDate,
options.startDayHour,
options.startDate,
options.intervalCount,
);
}
packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_month.ts:70
calculateStartViewDateoverrides a protected hook fromViewDataGenerator, but it’s declared without an access modifier here (so it becomes public). After dropping the underscore prefix, consider marking this override asprotectedto keep it as an internal extension point.
calculateStartViewDate(options) {
return monthUtils.calculateStartViewDate(
options.currentDate,
options.startDayHour,
options.startDate,
packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_month.ts:80
setVisibilityDatesoverrides a protected hook fromViewDataGenerator, but it’s declared without an access modifier here (so it becomes public). Consider making this methodprotectedto avoid expanding the class’s public API now that the underscore prefix is removed.
setVisibilityDates(options) {
const {
intervalCount,
startDate,
currentDate,
| calculateStartViewDate(options) { | ||
| return weekUtils.calculateStartViewDate( | ||
| options.currentDate, | ||
| options.startDayHour, | ||
| options.startDate, | ||
| this._getIntervalDuration(options.intervalCount), | ||
| this.getIntervalDuration(options.intervalCount), | ||
| this.getFirstDayOfWeek(options.firstDayOfWeek), | ||
| ); |
There was a problem hiding this comment.
calculateStartViewDate overrides a protected hook from ViewDataGenerator, but here it is declared without an access modifier (making it public). Since the underscore prefix was removed, this unintentionally widens the apparent public API of the generator; consider marking this override as protected (matching the base class visibility) to keep it as an internal customization point.
| calculateStartViewDate(options) { | ||
| return dayUtils.calculateStartViewDate( | ||
| options.currentDate, | ||
| options.startDayHour, | ||
| options.startDate, | ||
| this._getIntervalDuration(options.intervalCount), | ||
| this.getIntervalDuration(options.intervalCount), | ||
| ); | ||
| } |
There was a problem hiding this comment.
calculateStartViewDate overrides a protected hook from ViewDataGenerator, but it is currently declared without an access modifier (so it becomes public). With the underscore removed, it’s better to keep this as protected to avoid exposing it as part of the public surface of the class.
| calculateStartViewDate(options) { | ||
| return workWeekUtils.calculateStartViewDate( | ||
| options.currentDate, | ||
| options.startDayHour, | ||
| options.startDate, | ||
| this._getIntervalDuration(options.intervalCount), | ||
| this.getIntervalDuration(options.intervalCount), | ||
| this.getFirstDayOfWeek(options.firstDayOfWeek), | ||
| ); |
There was a problem hiding this comment.
calculateStartViewDate overrides a protected hook from the base generator, but it is declared as public here (no access modifier). To avoid widening the class API after removing the underscore prefix, consider marking it protected to match ViewDataGenerator.
| appointmentDataSource!: AppointmentDataSource; | ||
|
|
||
| _dataSource: any; | ||
| dataSource: any; |
There was a problem hiding this comment.
Scheduler includes DataHelperMixin, which reads/writes the data source via this._dataSource. Renaming the instance field to dataSource means the mixin will still populate this._dataSource, while the scheduler code will read this.dataSource (undefined), breaking loading, filtering, and reload flows. Consider keeping the _dataSource field (or adding a dataSource getter/setter that proxies to _dataSource) and updating the ESLint allowlist instead of renaming the storage field.
| dataSource: any; | |
| _dataSource: any; |
| @@ -741,7 +744,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { | |||
| visible && this._dimensionChanged(null, true); | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
DataHelperMixin expects override hooks named _dataSourceOptions and _dataSourceChangedHandler (see js/__internal/data/m_data_helper.ts). Renaming these to dataSourceOptions / dataSourceChangedHandler means the mixin will no longer call them, so data source options won’t be applied and change notifications won’t refresh appointments. Recommendation: keep the underscore-prefixed hook names (and allowlist them for the scheduler naming-convention rule) or provide underscore-named wrappers that delegate to the new methods.
| _dataSourceOptions() { | |
| return this.dataSourceOptions(); | |
| } |
| // m_scheduler.ts | ||
| '_appointments', | ||
| '_dataAccessors', | ||
| '_getDragBehavior', | ||
| '_isAppointmentBeingUpdated', | ||
| '_options', | ||
| '_workSpace', | ||
|
|
There was a problem hiding this comment.
The scheduler naming-convention allowlist is missing DataHelperMixin hook/member names (_dataSource, _dataSourceOptions, _dataSourceChangedHandler). Because scheduler widgets include DataHelperMixin, these underscore members need to remain available to preserve behavior; otherwise you end up renaming required mixin hooks in m_scheduler.ts. Suggest adding these names to schedulerLegacyMembers (or a dedicated list) so the lint rule can forbid other underscore members without breaking DataHelperMixin integration.
… _dataSourceLoadedCallback
…n to Widget overrides
| visible && this._dimensionChanged(null, true); | ||
| } | ||
|
|
||
| _dataSourceOptions() { | ||
| dataSourceOptions() { | ||
| return { paginate: false }; | ||
| } | ||
|
|
There was a problem hiding this comment.
Scheduler includes DataHelperMixin, which looks specifically for _dataSourceOptions and _dataSourceChangedHandler methods (see js/__internal/data/m_data_helper.ts). Renaming them to dataSourceOptions / dataSourceChangedHandler means the mixin will no longer call these hooks, and there is still an internal call to this._dataSourceChangedHandler(...) in _initMarkup (around line 1090), which will now be undefined. Please restore the underscore method names (and, if needed for the new lint rule, add them to the scheduler allowlist) or add underscore-named wrapper methods delegating to the new names, and make sure all call sites use the same method names.
| this._renderContentImpl(); | ||
| } | ||
|
|
||
| _dataSourceChangedHandler(result?: Appointment[]) { | ||
| dataSourceChangedHandler(result?: Appointment[]) { | ||
| if (this.readyToRenderAppointments) { | ||
| this.workSpaceRecalculation.done(() => { | ||
| this._layoutManager.prepareAppointments(result); | ||
| this.layoutManager.prepareAppointments(result); | ||
| this.renderAppointments(); | ||
| this.updateA11yStatus(); |
There was a problem hiding this comment.
dataSourceChangedHandler was renamed, but _initMarkup still calls this._dataSourceChangedHandler(...) (around line 1090). Unless the underscore-named method remains (as required by DataHelperMixin), this will throw at runtime. Please align the method name and the call sites; if you keep the underscore method for framework compatibility, consider having dataSourceChangedHandler delegate to _dataSourceChangedHandler (or vice versa) to avoid future drift.
No description provided.