blob: bd1ff9c58e139b2b6850c5954103a58d355c1ede [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package apidiff
import (
"encoding/json"
"fmt"
"io"
"go.fuchsia.dev/fuchsia/tools/fidl/lib/summarize"
)
// Classification describes the compatibility of a change.
type Classification string
const (
// NeedsBackfill means the change could not be classified on its own, but
// requires another pass where its enclosing declaration is known. See
// backfillForParentStrictness().
NeedsBackfill Classification = "NeedsBackfill"
// Compatible means the change is both API and ABI compatible.
Compatible Classification = "Compatible"
// APIBreaking means the change will break compilation. It might also break
// ABI; we don't distinguish between API breaks and API+ABI breaks.
APIBreaking Classification = "APIBreaking"
// APICompatibleButABIBreaking is used to reject the small category of
// changes which break ABI without also breaking API.
APICompatibleButABIBreaking Classification = "APICompatibleButABIBreaking"
)
// ReportItem is a single line item of the API diff report.
type ReportItem struct {
// Name is the fully qualified name that this report item
// pertains to.
Name summarize.Name `json:"name"`
// Before is what the API summary used to look like.
Before *summarize.ElementStr `json:"before,omitempty"`
// After is what the API summary looks like now.
After *summarize.ElementStr `json:"after,omitempty"`
// Conclusion is the finding.
Conclusion Classification `json:"conclusion"`
}
func (r ReportItem) IsAdd() bool {
return r.Before == nil && r.After != nil
}
func (r ReportItem) IsRemove() bool {
return r.Before != nil && r.After == nil
}
func (r ReportItem) IsChange() bool {
return r.Before != nil && r.After != nil
}
// Report is a top-level wrapper for the API diff result.
type Report struct {
// APIDiff has the report items for each individual change of the API
// surface for a FIDL library.
APIDiff []ReportItem `json:"api_diff,omitempty"`
// backfillIndexes contains APIDiff indexes which have a NeedsBackfill
// classification, to be handled by backfillForParentStrictness().
backfillIndexes []int
}
// backfillForParentStrictness backfills all APIDiff indexes based on the given
// strictness. This is called when processing a declaration, after having
// already processed its members (and put them in r.backfillIndexes).
func (r *Report) backfillForParentStrictness(isStrict bool) {
for _, i := range r.backfillIndexes {
if isStrict {
r.APIDiff[i].Conclusion = APIBreaking
} else {
r.APIDiff[i].Conclusion = Compatible
}
}
r.backfillIndexes = nil
}
func (r *Report) addToDiff(rep ReportItem) {
switch rep.Conclusion {
case "":
panic(fmt.Sprintf("unset conclusion: %+v", rep))
case NeedsBackfill:
r.backfillIndexes = append(r.backfillIndexes, len(r.APIDiff))
}
r.APIDiff = append(r.APIDiff, rep)
}
// WriteJSON writes a report as JSON.
func (r Report) WriteJSON(w io.Writer) error {
e := json.NewEncoder(w)
e.SetEscapeHTML(false)
e.SetIndent("", " ")
if err := e.Encode(r); err != nil {
return fmt.Errorf("while writing JSON: %w", err)
}
return nil
}
// add processes a single added ElementStr.
func (r *Report) add(item *summarize.ElementStr) {
ret := ReportItem{
Name: item.Name,
After: item,
}
switch item.Kind {
case summarize.BitsKind,
summarize.EnumKind,
summarize.StructKind,
summarize.LibraryKind,
summarize.ConstKind,
summarize.TableKind,
summarize.UnionKind,
summarize.ProtocolKind,
summarize.AliasKind,
summarize.TableMemberKind,
summarize.BitsMemberKind:
ret.Conclusion = Compatible
case summarize.EnumMemberKind, summarize.UnionMemberKind, summarize.StructMemberKind:
// Adding a member to a struct or a strict enum/union is compatible iff
// the parent struct/enum/union is also being added, i.e. this is one of
// its initial members. Adding a member to a flexible enum/union is
// always compatible. We need to backfill for this information.
ret.Conclusion = NeedsBackfill
case summarize.ProtocolMemberKind:
// TODO(https://fxbug.dev/42078310): Technically, adding a method is API breaking
// because server implementations will fail to compile if they don't
// handle the new method. To avoid this you can use the @transitional
// attribute, and remove it once all servers are updated. We are not
// tracking this now for two reasons:
//
// 1. It's common to add methods to protocols that have no server
// implementations outside the platform. Requiring @transitional
// in these cases is unnecessary and will slow us down.
//
// 2. Removing the @transitional attribute requires duplicating the
// method definition: https://fuchsia.dev/fuchsia-src/reference/fidl/language/versioning?hl=en#swapping.
// We should improve the syntax (e.g. integrate into @available)
// before requiring all new methods to be @transitional.
ret.Conclusion = Compatible
default:
panic(fmt.Sprintf("unexpected item kind: %+v", item))
}
r.addToDiff(ret)
}
// remove processes a single removed ElementStr.
func (r *Report) remove(item *summarize.ElementStr) {
ret := ReportItem{
Name: item.Name,
Before: item,
Conclusion: APIBreaking,
}
r.addToDiff(ret)
}
func (r *Report) compare(before, after *summarize.ElementStr) {
if *before == *after {
// No change
return
}
ret := ReportItem{
Name: after.Name,
Before: before,
After: after,
}
switch {
case before.Name != after.Name:
panic(fmt.Sprintf("before name %q != after name %q", before.Name, after.Name))
case before.Kind != after.Kind,
before.Strictness != after.Strictness,
before.Resourceness != after.Resourceness,
before.Type != after.Type,
before.Value != after.Value,
before.Openness != after.Openness,
before.Transport != after.Transport,
before.Direction != after.Direction,
before.Request != after.Request,
before.Response != after.Response,
before.Error != after.Error:
ret.Conclusion = APIBreaking
case before.Ordinal != after.Ordinal:
if before.Kind == "struct/member" {
// Reordering struct members breaks API due to positional
// initializers in C++. (It of course breaks ABI too.)
ret.Conclusion = APIBreaking
} else {
// Changing table/union/method ordinals breaks ABI only.
ret.Conclusion = APICompatibleButABIBreaking
}
default:
panic(fmt.Sprintf("unexpected difference: before = %+v, after = %+v", before, after))
}
r.addToDiff(ret)
}