125 lines
5.8 KiB
Go
125 lines
5.8 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package interfacesnapshot
|
|
|
|
// flagTypeChange identifies one flag type migration exactly: which command,
|
|
// which flag, and in which direction. Nothing is matched by wildcard — an entry
|
|
// that differs in any of the four fields does not apply.
|
|
//
|
|
// CommandPath is the canonical Command.Path form, which always includes the root
|
|
// command name ("dws minutes permission apply"). It is deliberately *not* the
|
|
// alias-expanded accepted path: a command reachable through an alias enters
|
|
// compareEffectiveFlags once per accepted spelling, and keying on the accepted
|
|
// path would let every alias spelling bypass the exemption and re-report the
|
|
// change.
|
|
//
|
|
// This table is duplicated in scripts/policy/interface-baseline/reviewed.go and
|
|
// the two must stay identical; TestCrossPlatformCoverageReviewedFlagTypeTableMatchesInterfaceBaseline
|
|
// fails if they drift. The duplication is forced rather than chosen:
|
|
// check-authoritative-interface-baselines.sh copies the whole
|
|
// scripts/policy/interface-baseline directory into a worktree checked out at a
|
|
// historical revision and builds it there, so that copy cannot import a package
|
|
// this branch adds.
|
|
type flagTypeChange struct {
|
|
CommandPath string
|
|
Flag string
|
|
From string
|
|
To string
|
|
}
|
|
|
|
// reviewedFlagTypeChanges enumerates the individually reviewed flag type
|
|
// migrations this gate accepts. A flag type is part of the published CLI
|
|
// contract, and a type swap cannot be proven safe from the type names alone —
|
|
// whether old invocations still parse depends on the concrete value domain the
|
|
// command enforces. So a migration is accepted only when this exact command,
|
|
// flag, and old→new pair appear here. Adding an entry is a contract decision
|
|
// and belongs in review, not in a feature change.
|
|
//
|
|
// Entries are direction-sensitive by construction: "string" → "int" is a
|
|
// separate key from "int" → "string" and only the reviewed direction is
|
|
// accepted. The reverse is not a candidate for this table — widening an int
|
|
// flag back to string lets values the parser used to reject reach RunE, which
|
|
// is a genuine loosening of validation rather than a type migration.
|
|
//
|
|
// The table alone does not admit a change: reviewedFlagTypeChange is consulted
|
|
// only when nothing else about the flag moved. See compareEffectiveFlags.
|
|
var reviewedFlagTypeChanges = map[flagTypeChange]struct{}{
|
|
// "dws minutes permission apply --policy" moved from a String flag parsed
|
|
// with strconv.ParseInt(v, 10, 64) in RunE to a native Int flag, keeping the
|
|
// same [2,4] domain check in RunE.
|
|
//
|
|
// This is admissible because the historical set of *successful* invocations
|
|
// is a subset of the new one, not merely similar to it:
|
|
//
|
|
// - Old: the value had to parse as base-10 and land in [2,4]. That is
|
|
// exactly "2", "3", "4" plus sign/leading-zero spellings of them
|
|
// ("+3", "03", "003", ...).
|
|
// - New: pflag's intValue.Set parses with strconv.ParseInt(s, 0, 64) —
|
|
// base 0 — and RunE still enforces [2,4]. Every base-10 spelling above
|
|
// resolves to the same number under base 0, so no historical success
|
|
// becomes a failure.
|
|
// - Base 0 additionally accepts "0x3" and friends, which the old parser
|
|
// rejected. That widens the accepted set; it cannot break a caller who
|
|
// was already succeeding.
|
|
// - Values outside [2,4] and non-numeric values still fail. Only the
|
|
// failure *message and timing* move, from RunE to flag parsing.
|
|
//
|
|
// The flag's default value changes from "" to "0" as an unavoidable
|
|
// consequence of the type. Neither gate compares defaults, and a default is
|
|
// not reachable by a caller here because RunE requires the flag to be
|
|
// explicitly set, so this is not a contract change.
|
|
{CommandPath: "dws minutes permission apply", Flag: "policy", From: "string", To: "int"}: {},
|
|
}
|
|
|
|
// reviewedFlagTypeChange reports whether this exact command, flag and direction
|
|
// is a reviewed migration. Callers must additionally establish that the rest of
|
|
// the flag's contract is unchanged before skipping the failure.
|
|
func reviewedFlagTypeChange(commandPath, flag, from, to string) bool {
|
|
_, reviewed := reviewedFlagTypeChanges[flagTypeChange{
|
|
CommandPath: commandPath,
|
|
Flag: flag,
|
|
From: from,
|
|
To: to,
|
|
}]
|
|
return reviewed
|
|
}
|
|
|
|
// flagContractOtherwiseChanged reports whether anything this gate checks about a
|
|
// flag, other than its type, differs between the two revisions. A reviewed type
|
|
// migration is only accepted when this returns false, so the exemption cannot
|
|
// carry an unrelated regression in behind it.
|
|
//
|
|
// Only the fields compareEffectiveFlags actually enforces are compared. Default
|
|
// is deliberately excluded: a string → int migration always changes it ("" to
|
|
// "0"), so including it would make every entry in the table dead. Deprecated is
|
|
// excluded for the same reason it is not a blocking change on its own.
|
|
func flagContractOtherwiseChanged(oldFlag, newFlag Flag) bool {
|
|
if !oldFlag.Required && newFlag.Required {
|
|
return true
|
|
}
|
|
if oldFlag.Shorthand != "" && newFlag.Shorthand != oldFlag.Shorthand {
|
|
return true
|
|
}
|
|
if oldFlag.NoOpt != "" && newFlag.NoOpt != oldFlag.NoOpt {
|
|
return true
|
|
}
|
|
if !oldFlag.Hidden && newFlag.Hidden {
|
|
return true
|
|
}
|
|
if oldFlag.AliasOf != "" && newFlag.AliasOf != oldFlag.AliasOf {
|
|
return true
|
|
}
|
|
return false
|
|
}
|