diff --git a/ivette/package.json b/ivette/package.json index 5c000eebf185db31c6cb64f668ae09dad48fbd1d..bedf12a40ddbf6d65752f15353aec760bfbebc31 100644 --- a/ivette/package.json +++ b/ivette/package.json @@ -58,6 +58,11 @@ "@types/diff": "", "@types/react-window": "", "codemirror": "^5.65.2", + "@codemirror/view": "6.2.3", + "@codemirror/state": "6.1.1", + "@codemirror/language": "6.2.1", + "@codemirror/commands": "6.1.0", + "@codemirror/lang-cpp": "6.0.1", "cytoscape": "", "cytoscape-cola": "", "cytoscape-cose-bilkent": "", diff --git a/ivette/src/dome/renderer/dark.css b/ivette/src/dome/renderer/dark.css index f53d8d9120eeed3dc74c02d0e7761f13c7cdea87..0660868825afc53f7e051a476f593e2d9c4d15f6 100644 --- a/ivette/src/dome/renderer/dark.css +++ b/ivette/src/dome/renderer/dark.css @@ -100,5 +100,11 @@ --eva-alarms-true: green; --eva-alarms-false: #fb4e4a; --eva-alarms-unknown: darkorange; + + --codemirror-comment: #748a8d; + --codemirror-type: #bacd5f; + --codemirror-number: #d3869b; + --codemirror-keyword: #d84954; + --codemirror-def: #94aadd; } } diff --git a/ivette/src/dome/renderer/data/arrays.ts b/ivette/src/dome/renderer/data/arrays.ts index adc862f8913c006a9b968afe6d69b17a5b7fd750..26076398cc91fa72ea59b797abe627eef7e896e5 100644 --- a/ivette/src/dome/renderer/data/arrays.ts +++ b/ivette/src/dome/renderer/data/arrays.ts @@ -64,3 +64,11 @@ export function mergeArraysByKey<A, B>( ): (A | A & B)[] { return mergeArrays(a1, a2, (x1, x2) => x1.key === x2.key); } + +/** Maps a function through an array and returns the first computed value that + is not undefined. */ +export type Maybe<A> = A | undefined; +export function first<X, R>(xs: X[], fn: (x: X) => Maybe<R>): Maybe<R> { + for (const x of xs) { const r = fn(x); if (r) return r; } + return undefined; +} diff --git a/ivette/src/dome/renderer/light.css b/ivette/src/dome/renderer/light.css index eead3201596027ab0934dc20d9f447e90b89dce9..0caf3fb0e549257228f9e19d34c17a8b73f51b34 100644 --- a/ivette/src/dome/renderer/light.css +++ b/ivette/src/dome/renderer/light.css @@ -100,5 +100,11 @@ --eva-alarms-true: green; --eva-alarms-false: #fb4e4a; --eva-alarms-unknown: darkorange; + + --codemirror-comment: #aa5500; + --codemirror-type: #008855; + --codemirror-number: #116644; + --codemirror-keyword: #770088; + --codemirror-def: #0000ff; } } diff --git a/ivette/src/dome/renderer/text/editor.tsx b/ivette/src/dome/renderer/text/editor.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6f0438512f5f3702932e122c44c7cbe4dcea7a48 --- /dev/null +++ b/ivette/src/dome/renderer/text/editor.tsx @@ -0,0 +1,456 @@ +/* ************************************************************************ */ +/* */ +/* This file is part of Frama-C. */ +/* */ +/* Copyright (C) 2007-2023 */ +/* CEA (Commissariat à l'énergie atomique et aux énergies */ +/* alternatives) */ +/* */ +/* you can redistribute it and/or modify it under the terms of the GNU */ +/* Lesser General Public License as published by the Free Software */ +/* Foundation, version 2.1. */ +/* */ +/* It is distributed in the hope that it will be useful, */ +/* but WITHOUT ANY WARRANTY; without even the implied warranty of */ +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ +/* GNU Lesser General Public License for more details. */ +/* */ +/* See the GNU Lesser General Public License version 2.1 */ +/* for more details (enclosed in the file licenses/LGPLv2.1). */ +/* */ +/* ************************************************************************ */ + +import React from 'react'; + +import { EditorState, StateField, Facet, Extension } from '@codemirror/state'; +import { Annotation, Transaction, RangeSet } from '@codemirror/state'; +import { EditorSelection, Text } from '@codemirror/state'; + +import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view'; +import { Decoration, DecorationSet } from '@codemirror/view'; +import { DOMEventMap as EventMap } from '@codemirror/view'; +import { GutterMarker, gutter } from '@codemirror/view'; +import { Tooltip, showTooltip } from '@codemirror/view'; +import { lineNumbers } from '@codemirror/view'; + +import { parser } from '@lezer/cpp'; +import { tags } from '@lezer/highlight'; +import { SyntaxNode } from '@lezer/common'; +import * as Language from '@codemirror/language'; + +import './style.css'; + +export type { Extension } from '@codemirror/state'; +export { GutterMarker } from '@codemirror/view'; +export { Decoration } from '@codemirror/view'; +export { RangeSet } from '@codemirror/state'; + + + +// ----------------------------------------------------------------------------- +// CodeMirror state's extensions types definitions +// ----------------------------------------------------------------------------- + +// Helper types definitions. +export type View = EditorView | null; +export type Range = { from: number, to: number }; +export type Set<A> = (view: View, value: A) => void; +export type Get<A> = (state: EditorState | undefined) => A; +export type IsUpdated = (update: ViewUpdate) => boolean; +export interface Struct<S> { structure: S, extension: Extension } +export interface Value<A> { init: A, get: Get<A> } +export interface Data<A, S> extends Value<A>, Struct<S> { isUpdated: IsUpdated } + +// Event handlers type definition. +export type Handler<I, E> = (i: I, v: EditorView, e: E) => void; +export type Handlers<I> = { [e in keyof EventMap]?: Handler<I, EventMap[e]> }; + +// A Field is a data added to the CodeMirror internal state that can be +// modified by the outside world and used by plugins. The typical use case is +// when one needs to inject information from the server into the CodeMirror +// component. A Field exposes a getter and a setter that handles all React's +// hooks shenanigans. It also exposes a StateField, a CodeMirror data structure +// representing the internal state's part responsible of the data. This +// structure is exposed for two reasons. The first one is that it contains the +// extension that must be added to the CodeMirror instanciation. The second one +// is that it is needed during the Aspects creation's process. +export interface Field<A> extends Data<A, StateField<A>> { set: Set<A> } + +// An Aspect is a data associated with an editor state and computed by combining +// data from several fields. A typical use case is if one needs a data that +// relies on a server side information (like a synchronized array) which must be +// recomputed each time the selection (which is a field but is also an internal +// information of CodeMirror) is changed. An Aspect exposes a getter that +// handles all React's hooks shenanigans and an extension that must be added to +// the CodeMirror initial configuration. +export type Aspect<A> = Data<A, Facet<A, A>>; + +// State extensions and Aspects have to declare their dependencies, i.e. the +// Field and Aspects they rely on to perform their computations. Dependencies +// are declared as a record mapping names to either a Field or an Aspect. This +// is needed to be able to give the dependencies values to the computing +// functions in a typed manner. However, an important point to take into +// consideration is that the extensions constructors defined below cannot +// actually typecheck without relying on type assertions. It means that if you +// declare an extension's dependencies after creating the extension, it will +// crash at execution time. So please, check that every dependency is declared +// before being used. +export type Dict = Record<string, unknown>; +export type Dependency<A> = Field<A> | Aspect<A>; +export type Dependencies<I extends Dict> = { [K in keyof I]: Dependency<I[K]> }; + +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// Internal types and helpers +// ----------------------------------------------------------------------------- + +// Type aliases to shorten internal definitions. +type Dep<A> = Dependency<A>; +type Deps<I extends Dict> = Dependencies<I>; +type Combine<Output> = (l: readonly Output[]) => Output; +type Pred<I extends Dict> = (d: Dep<I[typeof k]>, k: string) => boolean; +type Mapper<I extends Dict, A> = (d: Dep<I[typeof k]>, k: string) => A; +type Transform<I extends Dict> = Mapper<I, unknown>; + +// Helper function used to map a function over Dependencies. +function mapDeps<I extends Dict, A>(deps: Deps<I>, fn: Mapper<I, A>): A[] { + return Object.keys(deps).map((k) => fn(deps[k], k)); +} + +// Helper function used to check if at least one depencency satisfied a +// given predicate. +function existsDeps<I extends Dict>(deps: Deps<I>, fn: Pred<I>): boolean { + return Object.keys(deps).find((k) => fn(deps[k], k)) !== undefined; +} + +// Helper function used to transfrom a Dependencies will keeping its structure. +function transformDeps<I extends Dict>(deps: Deps<I>, tr: Transform<I>): Dict { + return Object.fromEntries(Object.keys(deps).map(k => [k, tr(deps[k], k)])); +} + +// Helper function retrieving the current values associated to each dependencies +// in a given editor state. They are returned as a Dict instead of the precise +// type because of TypeScript subtyping shenanigans that prevent us to correctly +// type the returned record. Thus, a type assertion has to be used. +function inputs<I extends Dict>(ds: Deps<I>, s: EditorState | undefined): Dict { + return transformDeps(ds, (d) => d.get(s)); +} + +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// CodeMirror state's extensions +// ----------------------------------------------------------------------------- + +// Several extensions constructors are provided. Each one of them encapsulates +// its dependencies if needed. This means that adding an extension to the +// CodeMirror's initial configuration will also add all its dependencies' +// extensions, and thus recursively. However, for now, there is no systemic way +// to check that the fields at the root of the dependencies tree are updated by +// a component. This means you have to verify, by hands, that every field is +// updated when needed by your component. It may be possible to compute a +// function that asks for a value for each fields in the dependencies tree and +// does the update for you, thus forcing you to actually update every fields. +// But it seems hard to define and harder to type. + +// A Field is simply declared using an initial value. However, to be able to +// use it, you must add its extension (obtained through <field.extension>) to +// the CodeMirror initial configuration. If determining equality between +// values of the given type cannot be done using (===), an equality test can be +// provided through the optional parameters <equal>. Providing an equality test +// for complex types can help improve performances by avoiding recomputing +// extensions depending on the field. +export function createField<A>(init: A): Field<A> { + const annot = Annotation.define<A>(); + const create = (): A => init; + type Update<A> = (current: A, transaction: Transaction) => A; + const update: Update<A> = (current, tr) => tr.annotation(annot) ?? current; + const field = StateField.define<A>({ create, update }); + const get: Get<A> = (state) => state?.field(field) ?? init; + const set: Set<A> = (v, a) => v?.dispatch({ annotations: annot.of(a) }); + const isUpdated: IsUpdated = (update) => + update.transactions.find((tr) => tr.annotation(annot)) !== undefined; + return { init, get, set, structure: field, extension: field, isUpdated }; +} + +// An Aspect is declared using its dependencies and a function. This function's +// input is a record containing, for each key of the dependencies record, a +// value of the type of the corresponding field. The function's output is a +// value of the aspect's type. +export function createAspect<I extends Dict, O>( + deps: Dependencies<I>, + fn: (input: I) => O, +): Aspect<O> { + const enables = mapDeps(deps, (d) => d.extension); + const init = fn(transformDeps(deps, (d) => d.init) as I); + const combine: Combine<O> = (l) => l.length > 0 ? l[l.length - 1] : init; + const facet = Facet.define<O, O>({ combine, enables }); + const get: Get<O> = (state) => state?.facet(facet) ?? init; + const convertedDeps = mapDeps(deps, (d) => d.structure); + const compute: Get<O> = (s) => fn(inputs(deps, s) as I); + const extension = facet.compute(convertedDeps, compute); + const isUpdated: IsUpdated = (update) => + existsDeps(deps, (d) => d.isUpdated(update)); + return { init, get, structure: facet, extension, isUpdated }; +} + +// A Decorator is an extension that adds decorations to the CodeMirror's +// document, i.e. it tags subpart of the document with CSS classes. See the +// CodeMirror's documentation on Decoration for further details. +export function createDecorator<I extends Dict>( + deps: Dependencies<I>, + fn: (inputs: I, state: EditorState) => DecorationSet +): Extension { + const enables = mapDeps(deps, (d) => d.extension); + const get = (s: EditorState): DecorationSet => fn(inputs(deps, s) as I, s); + class S { s: DecorationSet = RangeSet.empty; } + class D extends S { update(u: ViewUpdate): void { this.s = get(u.state); } } + const decorations = (d: D): DecorationSet => d.s; + return enables.concat(ViewPlugin.fromClass(D, { decorations })); +} + +// A Gutter is an extension that adds decorations (bullets or any kind of +// symbol) in a gutter in front of document's lines. See the CodeMirror's +// documentation on GutterMarker for further details. +export function createGutter<I extends Dict>( + deps: Dependencies<I>, + className: string, + line: (inputs: I, block: Range, view: EditorView) => GutterMarker | null +): Extension { + const enables = mapDeps(deps, (d) => d.extension); + const extension = gutter({ + class: className, + lineMarkerChange: (u) => existsDeps(deps, (d) => d.isUpdated(u)), + lineMarker: (view, block) => { + return line(inputs(deps, view.state) as I, block, view); + } + }); + return enables.concat(extension); +} + +// A Tooltip is an extension that adds decorations as a floating DOM element +// above or below some text. See the CodeMirror's documentation on Tooltip +// for further details. +export function createTooltip<I extends Dict>( + deps: Dependencies<I>, + fn: (input: I) => Tooltip | Tooltip[] | undefined, +): Extension { + const { structure, extension } = createAspect(deps, fn); + const show = showTooltip.computeN([structure], st => { + const tip = st.facet(structure); + if (tip === undefined) return [null]; + if ('length' in tip) return tip; + return [tip]; + }); + return [extension, show]; +} + +// An Event Handler is an extention responsible of performing a computation each +// time a DOM event (like <mouseup> or <contextmenu>) happens. +export function createEventHandler<I extends Dict>( + deps: Dependencies<I>, + handlers: Handlers<I>, +): Extension { + const enables = mapDeps(deps, (d) => d.extension); + const domEventHandlers = Object.fromEntries(Object.keys(handlers).map((k) => { + const h = handlers[k] as Handler<I, typeof k>; + const fn = (e: typeof k, v: EditorView): void => + h(inputs(deps, v.state) as I, v, e); + return [k, fn]; + })); + return enables.concat(EditorView.domEventHandlers(domEventHandlers)); +} + +// A View updater is an extension that allows to modify the view each time a +// depencency is updated. For example, one could use this to change the cursor +// position when a Data is updated by the outside world. +export function createViewUpdater<I extends Dict>( + deps: Dependencies<I>, + fn: (input: I, view: View) => void, +): Extension { + const enables = mapDeps(deps, (d) => d.extension); + const listener = EditorView.updateListener.of((u) => { + if(!existsDeps(deps, (d) => d.isUpdated(u))) return; + const get = (b: boolean): EditorState => b ? u.state : u.startState; + const state: <X>(d: Dep<X>) => EditorState = (d) => get(d.isUpdated(u)); + const inputs = transformDeps(deps, (d) => d.get(state(d))) as I; + fn(inputs, u.view); + }); + return enables.concat(listener); +} + +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// Code highlighting and parsing +// ----------------------------------------------------------------------------- + +// Plugin specifying how to highlight the code. The theme is handled by the CSS. +const Highlight = Language.syntaxHighlighting(Language.HighlightStyle.define([ + { tag: tags.comment, class: 'cm-comment' }, + { tag: tags.typeName, class: 'cm-type' }, + { tag: tags.number, class: 'cm-number' }, + { tag: tags.controlKeyword, class: 'cm-keyword' }, + { tag: tags.definition(tags.variableName) , class: 'cm-def' }, +])); + +// A language provider based on the [Lezer C++ parser], extended with +// highlighting and folding information. Only comments can be folded. +// (Source: https://github.com/lezer-parser/cpp) +const comment = (t: SyntaxNode): Range => ({ from: t.from + 2, to: t.to - 2}); +const folder = Language.foldNodeProp.add({ BlockComment: comment }); +const stringPrefixes = [ "L", "u", "U", "u8", "LR", "UR", "uR", "u8R", "R" ]; +const cppLanguage = Language.LRLanguage.define({ + parser: parser.configure({ props: [ folder ] }), + languageData: { + commentTokens: { line: "//", block: { open: "/*", close: "*/" } }, + indentOnInput: /^\s*(?:case |default:|\{|\})$/, + closeBrackets: { stringPrefixes }, + } +}); + +// This extension enables all the language highlighting features. +export const LanguageHighlighter: Extension = + [Highlight, new Language.LanguageSupport(cppLanguage)]; + +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// Standard extensions and commands +// ----------------------------------------------------------------------------- + +export const Selection = createSelectionField(); +function createSelectionField(): Field<EditorSelection> { + const cursor = EditorSelection.cursor(0); + const field = createField<EditorSelection>(EditorSelection.create([cursor])); + const set: Set<EditorSelection> = (view, selection) => { + view?.dispatch({ selection }); + field.set(view, selection); + }; + const updater = EditorView.updateListener.of((update) => { + if (update.selectionSet) field.set(update.view, update.state.selection); + }); + return { ...field, set, extension: [field.extension, updater] }; +} + +export const Document = createDocumentField(); +function createDocumentField(): Field<Text> { + const field = createField<Text>(Text.empty); + const set: Set<Text> = (view, text) => { + const selection = { anchor: 0 }; + const length = view?.state.doc.length; + const changes = { from: 0, to: length, insert: text }; + view?.dispatch({ changes, selection }); + field.set(view, text); + }; + const updater = EditorView.updateListener.of((update) => { + if (update.docChanged) field.set(update.view, update.state.doc); + }); + return { ...field, set, extension: [field.extension, updater] }; +} + +// Create a text field that updates the CodeMirror document when set. +export type ToString<A> = (text: A) => string; +export function createTextField<A>(init: A, toString: ToString<A>): Field<A> { + const field = createField<A>(init); + const set: Set<A> = (view, text) => { + const selection = { anchor: 0 }; + const length = view?.state.doc.length; + const changes = { from: 0, to: length, insert: toString(text) }; + view?.dispatch({ changes, selection }); + field.set(view, text); + }; + return { ...field, set }; +} + +// An extension displaying line numbers in a gutter. Does not display anything +// if the document is empty. +export const LineNumbers = createLineNumbers(); +function createLineNumbers(): Extension { + return lineNumbers({ + formatNumber: (lineNo, state) => { + if (state.doc.length === 0) return ''; + return lineNo.toString(); + } + }); +} + +// An extension highlighting the active line. +export const HighlightActiveLine = createHighlightActiveLine(); +function createHighlightActiveLine(): Extension { + const highlight = Decoration.line({ class: 'cm-active-line' }); + return createDecorator({}, (_, state) => { + if (state.doc.length === 0) return RangeSet.empty; + const { from } = state.doc.lineAt(state.selection.main.from); + const deco = highlight.range(from, from); + return RangeSet.of(deco); + }); +} + +// An extension handling the folding of foldable nodes. For exemple, If used +// with the language highlighter defined above, it will provides interactions +// to fold comments only. +export const FoldGutter = createFoldGutter(); +function createFoldGutter(): Extension { + return Language.foldGutter(); +} + +// Folds all the foldable nodes of the given view. +export function foldAll(view: View): void { + if (view !== null) Language.foldAll(view); +} + +// Unfolds all the foldable nodes of the given view. +export function unfoldAll(view: View): void { + if (view !== null) Language.unfoldAll(view); +} + +// Move to the given line. The indexation starts at 1. +export function selectLine(view: View, line: number, atTop: boolean): void { + if (!view || view.state.doc.lines < line) return; + const doc = view.state.doc; + const { from: here } = doc.lineAt(view.state.selection.main.from); + const { from: goto } = doc.line(Math.max(line, 1)); + if (here === goto) return; + view.dispatch({ selection: { anchor: goto }, scrollIntoView: true }); + if (!atTop) return; + const effects = EditorView.scrollIntoView(goto, { y: 'start', yMargin: 0 }); + view.dispatch({ effects }); +} + +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// Editor component +// ----------------------------------------------------------------------------- + +export interface EditorComponentProps { style?: React.CSSProperties } +export type EditorComponent = (props: EditorComponentProps) => JSX.Element; +export interface Editor { view: View; Component: EditorComponent } + +export function Editor(extensions: Extension[]): Editor { + const parent = React.useRef(null); + const editor = React.useRef<View>(null); + const Component: EditorComponent = React.useCallback((props) => { + return <div className='cm-global-box' style={props.style} ref={parent} />; + }, [parent]); + React.useEffect(() => { + if (!parent.current) return; + const state = EditorState.create({ extensions }); + editor.current = new EditorView({ state, parent: parent.current }); + }, [parent, extensions]); + return { view: editor.current, Component }; +} + +// ----------------------------------------------------------------------------- diff --git a/ivette/src/dome/renderer/text/style.css b/ivette/src/dome/renderer/text/style.css index 3dd0c7e07f48e46a4a51589d0fbbfa012e3617ad..d936197776803018537a588dc925c0d07239a09e 100644 --- a/ivette/src/dome/renderer/text/style.css +++ b/ivette/src/dome/renderer/text/style.css @@ -106,3 +106,87 @@ } /* -------------------------------------------------------------------------- */ +/* --- Styling CodeMirror 6 editor --- */ +/* -------------------------------------------------------------------------- */ + +.cm-global-box { + height: 100%; + overflow: auto; +} + +.cm-editor { + background-color: var(--background-report); + color: var(--text); + height: 100%; + width: 100%; + overflow: hidden; +} + +.cm-editor .cm-gutters { + border-right: 0px; + width: 2.15em; + background: var(--background-report); +} + +.cm-editor .cm-gutters .cm-lineNumbers { + color: var(--disabled-text); +} + +.cm-property-gutter { + width: 1.3em; + background: var(--code-bullet); +} + +.cm-hovered-code { + background-color: var(--code-hover); +} + +.cm-selected-code { + background-color: var(--code-select); +} + +.cm-selected-code:hover { + background-color: var(--code-select-hover); +} + +.cm-dead-code { + font-style: italic; + filter: opacity(0.7); +} + +.cm-non-term-code { + font-style: italic; + filter: opacity(0.7); +} + +.cm-tainted { + text-decoration: underline var(--warning) solid 1px; + text-decoration-skip-ink: none; + text-underline-position: under; +} + +.cm-tooltip.cm-tainted-tooltip { + background-color: var(--background-softer); + color: var(--text-highlighted); + border: 1px solid var(--background-profound); + padding: 2px 7px; + border-radius: 4px; +} + +.cm-tooltip-above.cm-tainted-tooltip .cm-tooltip-arrow:before { + border-top-color: var(--background-profound); +} + +.cm-tooltip-above.cm-tainted-tooltip .cm-tooltip-arrow:after { + border-top-color: var(--background-softer); +} + +.cm-def { color: var(--codemirror-def); } +.cm-comment { color: var(--codemirror-comment); } +.cm-type { color: var(--codemirror-type); } +.cm-number { color: var(--codemirror-number); } +.cm-keyword { color: var(--codemirror-keyword); } + +.cm-active-line { background-color: var(--background); } + +/* -------------------------------------------------------------------------- */ diff --git a/ivette/src/frama-c/kernel/ASTinfo.tsx b/ivette/src/frama-c/kernel/ASTinfo.tsx index 65f9243ea26e3bf825dea2738676bc7f9507ab00..2c3ea9a4b7cf4c769e9682690cc9106c00a1da9d 100644 --- a/ivette/src/frama-c/kernel/ASTinfo.tsx +++ b/ivette/src/frama-c/kernel/ASTinfo.tsx @@ -27,6 +27,7 @@ import React from 'react'; import * as Dome from 'dome'; import { classes } from 'dome/misc/utils'; +import * as Server from 'frama-c/server'; import * as States from 'frama-c/states'; import * as DATA from 'frama-c/kernel/api/data'; import * as AST from 'frama-c/kernel/api/ast'; @@ -120,9 +121,9 @@ interface InfoSectionProps { function MarkInfos(props: InfoSectionProps): JSX.Element { const [unfold, setUnfold] = React.useState(true); const [more, setMore] = React.useState(false); - const { marker, markerInfo } = props; - const allInfos: ASTinfos[] = - States.useRequest(AST.getInformation, marker) ?? []; + const { marker: m, markerInfo } = props; + const req = React.useMemo(() => Server.send(AST.getInformation, m), [m]); + const { result: allInfos = [] } = Dome.usePromise(req); const highlight = classes( props.selected && 'selected', props.hovered && 'hovered', diff --git a/ivette/src/frama-c/kernel/ASTview.tsx b/ivette/src/frama-c/kernel/ASTview.tsx index 6b6da1e69c78a49b32bdac5293eb5d663be07106..ed3deae15c9816ae754209387501b17526d59f6c 100644 --- a/ivette/src/frama-c/kernel/ASTview.tsx +++ b/ivette/src/frama-c/kernel/ASTview.tsx @@ -20,123 +20,341 @@ /* */ /* ************************************************************************ */ -// -------------------------------------------------------------------------- -// --- AST Source Code -// -------------------------------------------------------------------------- - import React from 'react'; -import _ from 'lodash'; -import CodeMirror from 'codemirror/lib/codemirror'; +import Lodash from 'lodash'; import * as Dome from 'dome'; -import * as Json from 'dome/data/json'; +import * as Editor from 'dome/text/editor'; +import * as Utils from 'dome/data/arrays'; +import * as Server from 'frama-c/server'; +import * as States from 'frama-c/states'; +import { key } from 'dome/data/json'; import * as Settings from 'dome/data/settings'; -import { TitleBar } from 'ivette'; import { IconButton } from 'dome/controls/buttons'; -import type { key } from 'dome/data/json'; -import { RichTextBuffer } from 'dome/text/buffers'; -import { Text } from 'dome/text/editors'; import { Filler, Inset } from 'dome/frame/toolbars'; - -import * as Preferences from 'ivette/prefs'; - -import * as Server from 'frama-c/server'; -import * as States from 'frama-c/states'; -import * as RichText from 'frama-c/richtext'; import * as Ast from 'frama-c/kernel/api/ast'; +import { text } from 'frama-c/kernel/api/data'; +import * as Eva from 'frama-c/plugins/eva/api/general'; import * as Properties from 'frama-c/kernel/api/properties'; -import { getCallers, getDeadCode } from 'frama-c/plugins/eva/api/general'; import { getWritesLval, getReadsLval } from 'frama-c/plugins/studia/api/studia'; -// -------------------------------------------------------------------------- -// --- Pretty Printing (Browser Console) -// -------------------------------------------------------------------------- - -const D = new Dome.Debug('AST View'); - -// -------------------------------------------------------------------------- -// --- Rich Text Printer -// -------------------------------------------------------------------------- - -async function loadAST( - buffer: RichTextBuffer, theFunction?: string, theMarker?: string, -): Promise<void> { - buffer.clear(); - if (theFunction) { - buffer.log('// Loading', theFunction, '…'); - (async () => { - try { - const data = await Server.send(Ast.printFunction, theFunction); - buffer.clear(); - if (!data) { - buffer.log('// No code for function', theFunction); - } - RichText.printTextWithTags(buffer, data); - if (theMarker) - buffer.scroll(theMarker); - } catch (err) { - D.error( - `Fail to retrieve the AST of function '${theFunction}' ` + - `and marker '${theMarker}':`, err, - ); - } - })(); - } +import { TitleBar } from 'ivette'; +import * as Preferences from 'ivette/prefs'; + + + +// ----------------------------------------------------------------------------- +// Utilitary types and functions +// ----------------------------------------------------------------------------- + +// An alias type for functions and locations. +type Fct = string | undefined; +type Marker = string | undefined; + +// A range is just a pair of position in the code. +type Range = Editor.Range; + +// Type checking that an input is defined. +function isDef<A>(a: A | undefined): a is A { return a !== undefined; } + +// Map a function over a list, removing all inputs that returned undefined. +function mapFilter<A, B>(xs: readonly A[], fn: (x: A) => B | undefined): B[] { + return xs.map(fn).filter(isDef); } -/* --------------------------------------------------------------------------*/ -/* --- Function Callers ---*/ -/* --------------------------------------------------------------------------*/ +// ----------------------------------------------------------------------------- + + -type Caller = { fct: key<'#fct'>, marker: key<'#stmt'> }; +// ----------------------------------------------------------------------------- +// Tree datatype definition and utiliary functions +// ----------------------------------------------------------------------------- -async function functionCallers(functionName: string): Promise<Caller[]> { - try { - const data = await Server.send(getCallers, functionName); - const locations = data.map(([fct, marker]) => ({ fct, marker })); - return locations; - } catch (err) { - D.error(`Fail to retrieve callers of function '${functionName}':`, err); - return []; +// The code is given by the server has a tree but implemented with arrays and +// without information on the ranges of each element. It will be converted in a +// good old tree that carry those information. +interface Leaf extends Range { text: string } +interface Node extends Range { id: string, children: Tree[] } +type Tree = Leaf | Node; + +// Utility functions on trees. +function isLeaf (t: Tree): t is Leaf { return 'text' in t; } +function isNode (t: Tree): t is Node { return 'id' in t && 'children' in t; } +const empty: Tree = { text: '', from: 0, to: 0 }; + +// Convert an Ivette text (i.e a function's code) into a Tree, adding range +// information to each construction. +function textToTree(t: text): Tree | undefined { + function aux(t: text, from: number): [Tree | undefined, number] { + if (t === null) return [undefined, from]; + if (typeof t === 'string') { + const to = from + t.length; + return [{ text: t, from, to }, to]; + } + if (t.length < 2 || typeof t[0] !== 'string') return [undefined, from]; + const children: Tree[] = []; let acc = from; + for (const child of t.slice(1)) { + const [node, to] = aux(child, acc); + if (node) children.push(node); + acc = to; + } + return [{ id: t[0], from, to: acc, children }, acc]; } + const [res] = aux(t, 0); + return res; } -/* --------------------------------------------------------------------------*/ -/* --- Studia Access ---*/ -/* --------------------------------------------------------------------------*/ +// Convert an Ivette text into a string to be displayed. +function textToString(text: text): string { + if (Array.isArray(text)) return text.slice(1).map(textToString).join(''); + else if (typeof text === 'string') return text; + else return ''; +} -type access = 'Reads' | 'Writes'; -interface StudiaInfos { - name: string, - title: string, - locations: { fct: key<'#fct'>, marker: Ast.marker }[], - index: number, +// Computes, for each markers of a tree, its range. Returns the map containing +// all those bindings. +function markersRanges(tree: Tree): Map<string, Range[]>{ + const ranges: Map<string, Range[]> = new Map(); + const toRanges = (tree: Tree): void => { + if (!isNode(tree)) return; + const trees = ranges.get(tree.id) ?? []; + trees.push(tree); + ranges.set(tree.id, trees); + for (const child of tree.children) toRanges(child); + }; + toRanges(tree); + return ranges; } -async function studia( - marker: string, - info: Ast.markerInfoData, - kind: access, -): Promise<StudiaInfos> { - const request = kind === 'Reads' ? getReadsLval : getWritesLval; - const data = await Server.send(request, marker); - const locations = data.direct.map(([f, m]) => ({ fct: f, marker: m })); - const lval = info.name; - if (locations.length > 0) { - const name = `${kind} of ${lval}`; - const acc = (kind === 'Reads') ? 'accessing' : 'modifying'; - const title = - `List of statements ${acc} the memory location pointed by ${lval}.`; - return { name, title, locations, index: 0 }; - } - const name = `No ${kind.toLowerCase()} of ${lval}`; - return { name, title: '', locations: [], index: 0 }; +function uniqueRange(m: string, rs: Map<string, Range[]>): Range | undefined { + const ranges = rs.get(m); + return (ranges && ranges.length > 0) ? ranges[0] : undefined; +} + +// Find the closest covering tagged node of a given position. Returns +// undefined if there is not relevant covering node. +function coveringNode(tree: Tree, pos: number): Node | undefined { + if (isLeaf(tree)) return undefined; + if (pos < tree.from || pos > tree.to) return undefined; + const child = Utils.first(tree.children, (c) => coveringNode(c, pos)); + if (child && isNode(child)) return child; + if (tree.from <= pos && pos < tree.to) return tree; + return undefined; +} + +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// Function code representation +// ----------------------------------------------------------------------------- + +// This field contains the current function's code as represented by Ivette. +// Its set function takes care to update the CodeMirror displayed document. +const Text = Editor.createTextField<text>(null, textToString); + +// This aspect computes the tree representing the currently displayed function's +// code, represented by the <Text> field. +const Tree = Editor.createAspect({ t: Text }, ({t}) => textToTree(t) ?? empty); + +// This aspect computes the markers ranges of the currently displayed function's +// tree, represented by the <Tree> aspect. +const Ranges = Editor.createAspect({ t: Tree }, ({ t }) => markersRanges(t)); + +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// Selected marker representation +// ----------------------------------------------------------------------------- + +// This field contains the currently selected function. +const Fct = Editor.createField<Fct>(undefined); + +// This field contains the currently selected marker. +const Marker = Editor.createField<Marker>(undefined); + +// The Ivette selection must be updated by CodeMirror plugins. This input +// add the callback in the CodeMirror internal state. +type UpdateSelection = (a: States.SelectionActions) => void; +const UpdateSelection = Editor.createField<UpdateSelection>(() => { return; }); + +// The marker field is considered as the ground truth on what is selected in the +// CodeMirror document. To do so, we catch the mouseup event (so when the user +// select a new part of the document) and update the Ivette selection +// accordingly. This will update the Marker field during the next Editor +// component's render and thus update everything else. +const MarkerUpdater = createMarkerUpdater(); +function createMarkerUpdater(): Editor.Extension { + const deps = { fct: Fct, tree: Tree, update: UpdateSelection }; + return Editor.createEventHandler(deps, { + mouseup: ({ fct, tree, update }, view, event) => { + const main = view.state.selection.main; + const id = coveringNode(tree, main.from)?.id; + const location = { fct, marker: Ast.jMarker(id) }; + update({ location }); + if (event.altKey) States.MetaSelection.emit(location); + } + }); +} + +// A View updater that scrolls the selected marker into view. It is needed to +// handle Marker's updates from the outside world, as they do not change the +// cursor position inside CodeMirror. +const MarkerScroller = createMarkerScroller(); +function createMarkerScroller(): Editor.Extension { + const deps = { marker: Marker, ranges: Ranges }; + return Editor.createViewUpdater(deps, ({ marker, ranges }, view) => { + if (!view || !marker) return; + const markerRanges = ranges.get(marker) ?? []; + if (markerRanges.length !== 1) return; + const { from: anchor } = markerRanges[0]; + view.dispatch({ selection: { anchor }, scrollIntoView: true }); + }); +} + +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// Hovered marker representation +// ----------------------------------------------------------------------------- + +// This field contains the currently hovered marker. +const Hovered = Editor.createField<Marker>(undefined); + +// The Ivette hovered element must be updated by CodeMirror plugins. This +// field add the callback in the CodeMirror internal state. +type UpdateHovered = (h: States.Hovered) => void; +const UpdateHovered = Editor.createField<UpdateHovered>(() => { return ; }); + +// The Hovered field is updated each time the mouse moves through the CodeMirror +// document. The handlers updates the Ivette hovered information, which is then +// reflected on the Hovered field by the Editor component itself. +const HoveredUpdater = createHoveredUpdater(); +function createHoveredUpdater(): Editor.Extension { + const deps = { fct: Fct, tree: Tree, update: UpdateHovered }; + return Editor.createEventHandler(deps, { + mousemove: (inputs, view, event) => { + const { fct, tree, update: updateHovered } = inputs; + const coords = { x: event.clientX, y: event.clientY }; + const reset = (): void => updateHovered(undefined); + const pos = view.posAtCoords(coords); + if (!pos) { reset(); return; } + const hov = coveringNode(tree, pos); + if (!hov) { reset(); return; } + const from = view.coordsAtPos(hov.from); + if (!from) { reset(); return; } + const to = view.coordsAtPos(hov.to); + if (!to) { reset(); return; } + const left = Math.min(from.left, to.left); + const right = Math.max(from.left, to.left); + const top = Math.min(from.top, to.top); + const bottom = Math.max(from.bottom, to.bottom); + const horizontallyOk = left <= coords.x && coords.x <= right; + const verticallyOk = top <= coords.y && coords.y <= bottom; + if (!horizontallyOk || !verticallyOk) { reset(); return; } + const marker = Ast.jMarker(hov.id); + updateHovered(marker ? { fct, marker } : undefined); + } + }); } -/* --------------------------------------------------------------------------*/ -/* --- Property Bullets ---*/ -/* --------------------------------------------------------------------------*/ +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// Plugin decorating hovered and selected elements +// ----------------------------------------------------------------------------- + +const CodeDecorator = createCodeDecorator(); +function createCodeDecorator(): Editor.Extension { + const hoveredClass = Editor.Decoration.mark({ class: 'cm-hovered-code' }); + const selectedClass = Editor.Decoration.mark({ class: 'cm-selected-code' }); + const deps = { ranges: Ranges, marker: Marker, hovered: Hovered }; + return Editor.createDecorator(deps, ({ ranges, marker: m, hovered: h }) => { + const hoveredRanges = h ? (ranges.get(h) ?? []) : []; + const selectedRanges = m ? (ranges.get(m) ?? []) : []; + const hovered = hoveredRanges.map(r => hoveredClass.range(r.from, r.to)); + const selected = selectedRanges.map(r => selectedClass.range(r.from, r.to)); + return Editor.RangeSet.of(selected.concat(hovered), true); + }); +} + +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// Dead code decorations plugin +// ----------------------------------------------------------------------------- + +// This field contains the dead code information as inferred by Eva. +const emptyDeadCode = { unreachable: [], nonTerminating: [] }; +const Dead = Editor.createField<Eva.deadCode>(emptyDeadCode); + +const DeadCodeDecorator = createDeadCodeDecorator(); +function createDeadCodeDecorator(): Editor.Extension { + const uClass = Editor.Decoration.mark({ class: 'cm-dead-code' }); + const tClass = Editor.Decoration.mark({ class: 'cm-non-term-code' }); + const deps = { dead: Dead, ranges: Ranges }; + return Editor.createDecorator(deps, ({ dead, ranges }) => { + const range = (m: string): Range[] | undefined => ranges.get(m); + const unreachableRanges = mapFilter(dead.unreachable, range).flat(); + const unreachable = unreachableRanges.map(r => uClass.range(r.from, r.to)); + const nonTermRanges = mapFilter(dead.nonTerminating, range).flat(); + const nonTerm = nonTermRanges.map(r => tClass.range(r.from, r.to)); + return Editor.RangeSet.of(unreachable.concat(nonTerm), true); + }); +} +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// Property bullets extension +// ----------------------------------------------------------------------------- + +// This field contains information on properties' tags. +type Tags = Map<string, States.Tag>; +const Tags = Editor.createField<Tags>(new Map()); + +// The component needs information on markers' status data. +const PropertiesStatuses = Editor.createField<Properties.statusData[]>([]); + +// This aspect filters all properties that does not have a valid range, and +// stores the remaining properties with their ranges. +const PropertiesRanges = createPropertiesRange(); +interface PropertyRange extends Properties.statusData { range: Range } +function createPropertiesRange(): Editor.Aspect<PropertyRange[]> { + const deps = { properties: PropertiesStatuses, ranges: Ranges }; + return Editor.createAspect(deps, ({ properties, ranges }) => { + const get = (p: Properties.statusData): Range[] => ranges.get(p.key) ?? []; + return properties.map(p => get(p).map(r => ({ ...p, range: r }))).flat(); + }); +} + +// This aspect computes the tag associated to each property. +const PropertiesTags = createPropertiesTags(); +function createPropertiesTags(): Editor.Aspect<Map<string, States.Tag>> { + const deps = { statuses: PropertiesStatuses, tags: Tags }; + return Editor.createAspect(deps, ({ statuses, tags }) => { + const res = new Map<string, States.Tag>(); + for (const p of statuses) { + if (!p.status) continue; + const tag = tags.get(p.status); + if (tag) res.set(p.key, tag); + } + return res; + }); +} + +// Bullet colors. function getBulletColor(status: States.Tag): string { switch (status.name) { case 'unknown': return '#FF8300'; @@ -154,187 +372,302 @@ function getBulletColor(status: States.Tag): string { } } -function makeBullet(status: States.Tag): HTMLDivElement { - const marker = document.createElement('div'); - marker.style.color = getBulletColor(status); - marker.style.textAlign = 'center'; - if (status.descr) - marker.title = status.descr; - marker.innerHTML = '◉'; - return marker; +// Property bullet gutter marker. +class PropertyBullet extends Editor.GutterMarker { + readonly bullet: HTMLDivElement; + toDOM(): HTMLDivElement { return this.bullet; } + constructor(status?: States.Tag) { + super(); + this.bullet = document.createElement('div'); + this.bullet.innerHTML = '◉'; + if (status) { + this.bullet.style.color = getBulletColor(status); + this.bullet.style.textAlign = 'center'; + if (status.descr) this.bullet.title = status.descr; + } + } } -// -------------------------------------------------------------------------- -// --- AST Printer -// -------------------------------------------------------------------------- +const PropertiesGutter = createPropertiesGutter(); +function createPropertiesGutter(): Editor.Extension { + const deps = { ranges: PropertiesRanges, propTags: PropertiesTags }; + const cls = 'cm-property-gutter'; + return Editor.createGutter(deps, cls, (inputs, block, view) => { + const { ranges, propTags } = inputs; + const line = view.state.doc.lineAt(block.from); + const start = line.from; const end = line.to; + const inLine = (r: Range): boolean => start <= r.from && r.to <= end; + function isHeader(r: Range): boolean { + if (!line.text.includes('requires')) return false; + const next = view.state.doc.line(line.number + 1); + return r.from <= next.from && next.to <= r.to; + } + const prop = ranges.find((r) => inLine(r.range) || isHeader(r.range)); + if (!prop) return null; + const statusTag = propTags.get(prop.key); + return statusTag ? new PropertyBullet(statusTag) : null; + }); +} -export default function ASTview(): JSX.Element { +// ----------------------------------------------------------------------------- - // Hooks - const buffer = React.useMemo(() => new RichTextBuffer(), []); - const printed = React.useRef<string | undefined>(); - const [selection, updateSelection] = States.useSelection(); - const [hoveredLoc] = States.useHovered(); - const multipleSelections = selection?.multiple.allSelections; - const theFunction = selection?.current?.fct; - const theMarker = selection?.current?.marker; - const hovered = hoveredLoc?.marker; - const [fontSize] = Settings.useGlobalSettings(Preferences.EditorFontSize); - const markersInfo = States.useSyncArray(Ast.markerInfo); - const deadCode = States.useRequest(getDeadCode, theFunction); - const propertyStatus = States.useSyncArray(Properties.status).getArray(); - const statusDict = States.useTags(Properties.propStatusTags); - - const setBullets = React.useCallback(() => { - propertyStatus.forEach((prop) => { - const status = statusDict.get(prop.status); - if (theFunction && prop.fct === theFunction && status) { - const bullet = makeBullet(status); - const markers = buffer.findTextMarker(prop.key); - buffer.forEach((cm) => { - markers.forEach((marker) => { - const line = marker.find()?.from.line; - if (line !== undefined) cm.setGutterMarker(line, 'bullet', bullet); - }); - }); - } - }); - }, [buffer, theFunction, propertyStatus, statusDict]); - - React.useEffect(() => { - setBullets(); - buffer.on('change', setBullets); - return () => { buffer.off('change', setBullets); }; - }, [buffer, setBullets]); - - const reload = React.useCallback(async () => { - printed.current = theFunction; - loadAST(buffer, theFunction, theMarker); - }, [buffer, theFunction, theMarker]); - - // Hook: async loading - React.useEffect(() => { if (printed.current !== theFunction) reload(); }); - - // Also reload the buffer when the AST is recomputed. - React.useEffect(() => { - Server.onSignal(Ast.changed, reload); - return () => { Server.offSignal(Ast.changed, reload); }; - }); - React.useEffect(() => { - const decorator = (marker: string): string | undefined => { - if (multipleSelections?.some((location) => location?.marker === marker)) - return 'highlighted-marker'; - if (deadCode?.unreachable?.some((m) => m === marker)) - return 'dead-code'; - if (deadCode?.nonTerminating?.some((m) => m === marker)) - return 'non-terminating'; - if (marker === hovered) - return 'hovered-marker'; - return undefined; - }; - buffer.setDecorator(decorator); - }, [buffer, multipleSelections, hovered, deadCode]); - - // Hook: marker scrolling - React.useEffect(() => { - if (theMarker) buffer.scroll(theMarker); - }, [buffer, theMarker]); - - function onHover(markerId?: string): void { - const marker = Json.jOption(Ast.jMarker)(markerId); - const fct = selection?.current?.fct; - States.setHovered(marker ? { fct, marker } : undefined); - } +// ----------------------------------------------------------------------------- +// Studia access +// ----------------------------------------------------------------------------- + +type access = 'Reads' | 'Writes'; + +interface StudiaProps { + marker: string, + info: Ast.markerInfoData, + kind: access, +} - function onSelection(markerId: string, meta = false): void { - const fct = selection?.current?.fct; - const location = { fct, marker: Ast.jMarker(markerId) }; - updateSelection({ location }); - if (meta) States.MetaSelection.emit(location); +interface StudiaInfos { + name: string, + title: string, + locations: { fct: key<'#fct'>, marker: Ast.marker }[], + index: number, +} + +async function studia(props: StudiaProps): Promise<StudiaInfos> { + const { marker, info, kind } = props; + const request = kind === 'Reads' ? getReadsLval : getWritesLval; + const data = await Server.send(request, marker); + const locations = data.direct.map(([f, m]) => ({ fct: f, marker: m })); + const lval = info.name; + if (locations.length > 0) { + const name = `${kind} of ${lval}`; + const acc = (kind === 'Reads') ? 'accessing' : 'modifying'; + const title = + `List of statements ${acc} the memory location pointed by ${lval}.`; + return { name, title, locations, index: 0 }; } + const name = `No ${kind.toLowerCase()} of ${lval}`; + return { name, title: '', locations: [], index: 0 }; +} - async function onContextMenu(markerId: string): Promise<void> { - const items = []; - const selectedMarkerInfo = markersInfo.getData(markerId); - if (selectedMarkerInfo?.var === 'function') { - if (selectedMarkerInfo.kind === 'declaration') { - const name = selectedMarkerInfo?.name; - if (name) { - const locations = await functionCallers(name); - const locationsByFunction = _.groupBy(locations, (e) => e.fct); - _.forEach(locationsByFunction, - (e) => { - const callerName = e[0].fct; - items.push({ - label: - `Go to caller ${callerName} ` + - `${e.length > 1 ? `(${e.length} call sites)` : ''}`, - onClick: () => updateSelection({ - name: `Call sites of function ${name}`, - locations, - index: locations.findIndex((l) => l.fct === callerName), - }), - }); +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// Context menu +// ----------------------------------------------------------------------------- + +// This field contains all the current function's callers, as inferred by Eva. +const Callers = Editor.createField<Eva.CallSite[]>([]); + +// This field contains information on markers. +type GetMarkerData = (key: string) => Ast.markerInfoData | undefined; +const GetMarkerData = Editor.createField<GetMarkerData>(() => undefined); + +const ContextMenuHandler = createContextMenuHandler(); +function createContextMenuHandler(): Editor.Extension { + const data = { tree: Tree, callers: Callers }; + const deps = { ...data, update: UpdateSelection, getData: GetMarkerData }; + return Editor.createEventHandler(deps, { + contextmenu: (inputs, view, event) => { + const { tree, callers, update, getData } = inputs; + const coords = { x: event.clientX, y: event.clientY }; + const position = view.posAtCoords(coords); if (!position) return; + const node = coveringNode(tree, position); + if (!node || !node.id) return; + const items: Dome.PopupMenuItem[] = []; + const info = getData(node.id); + if (info?.var === 'function') { + if (info.kind === 'declaration') { + const groupedCallers = Lodash.groupBy(callers, e => e.kf); + const locations = callers.map((l) => ({ fct: l.kf, marker: l.stmt })); + Lodash.forEach(groupedCallers, (e) => { + const callerName = e[0].kf; + const callSites = e.length > 1 ? `(${e.length} call sites)` : ''; + items.push({ + label: `Go to caller ${callerName} ` + callSites, + onClick: () => update({ + name: `Call sites of function ${info.name}`, + locations: locations, + index: locations.findIndex(l => l.fct === callerName) + }) }); + }); + } else { + const location = { fct: info.name }; + const onClick = (): void => update({ location }); + const label = `Go to definition of ${info.name}`; + items.push({ label, onClick }); } - } else { - items.push({ - label: `Go to definition of ${selectedMarkerInfo.name}`, - onClick: () => { - const location = { fct: selectedMarkerInfo.name }; - updateSelection({ location }); - }, - }); } + const enabled = info?.kind === 'lvalue' || info?.var === 'variable'; + const onClick = (kind: access): void => { + if (info && node.id) + studia({ marker: node.id, info, kind }).then(update); + }; + const reads = 'Studia: select reads'; + const writes = 'Studia: select writes'; + items.push({ label: reads, enabled, onClick: () => onClick('Reads') }); + items.push({ label: writes, enabled, onClick: () => onClick('Writes') }); + if (items.length > 0) Dome.popupMenu(items); + return; } - const enabled = selectedMarkerInfo?.kind === 'lvalue' - || selectedMarkerInfo?.var === 'variable'; - function onClick(kind: access): void { - if (selectedMarkerInfo) - studia( - markerId, - selectedMarkerInfo, - kind, - ).then(updateSelection); - } - items.push({ - label: 'Studia: select writes', - enabled, - onClick: () => onClick('Writes'), - }); - items.push({ - label: 'Studia: select reads', - enabled, - onClick: () => onClick('Reads'), - }); - if (items.length > 0) - Dome.popupMenu(items); - } + }); +} + +// ----------------------------------------------------------------------------- - const foldAll = (): void => buffer.forEach(CodeMirror.commands.foldAll); - const unfoldAll = (): void => buffer.forEach(CodeMirror.commands.unfoldAll); - const defaultFold = React.useCallback((): void => { - buffer.forEach((cm) => { CodeMirror.commands.unfoldAll(cm); }); - }, [buffer]); - React.useEffect(() => { - buffer.on('change', defaultFold); - return () => { buffer.off('change', defaultFold); }; +// ----------------------------------------------------------------------------- +// Tainted lvalues +// ----------------------------------------------------------------------------- + +type Taints = Eva.LvalueTaints; +const TaintedLvalues = Editor.createField<Taints[] | undefined>(undefined); + +function textOfTaint(taint: Eva.taintStatus): string { + switch (taint) { + case 'not_computed': return 'The taint has not been computed'; + case 'error': return 'There was an error during the taint computation'; + case 'not_applicable': return 'No taint for this lvalue'; + case 'direct_taint': return 'This lvalue can be affected by an attacker'; + case 'indirect_taint': + return 'This lvalue depends on path conditions that can \ + be affected by an attacker'; + case 'not_tainted': return 'This lvalue is safe'; + } + return ''; +} + +const TaintedLvaluesDecorator = createTaintedLvaluesDecorator(); +function createTaintedLvaluesDecorator(): Editor.Extension { + const mark = Editor.Decoration.mark({ class: 'cm-tainted' }); + const deps = { ranges: Ranges, tainted: TaintedLvalues }; + return Editor.createDecorator(deps, ({ ranges, tainted = [] }) => { + const find = (t: Taints): Range[] | undefined => ranges.get(t.lval); + const taintedRanges = mapFilter(tainted, find).flat(); + const marks = taintedRanges.map(r => mark.range(r.from, r.to)); + return Editor.RangeSet.of(marks, true); }); +} - const foldOptions: CodeMirror.FoldOptions = { - rangeFinder: (cm, pos) => { - const range = CodeMirror.fold.comment(cm, pos); - /* Allows folding multi-line comments only. */ - if (!range || range.from.line === range.to.line) return undefined; - return range; - } - }; +const TaintTooltip = createTaintTooltip(); +function createTaintTooltip(): Editor.Extension { + const deps = { hovered: Hovered, ranges: Ranges, tainted: TaintedLvalues }; + return Editor.createTooltip(deps, ({ hovered, ranges, tainted }) => { + const hoveredTaint = tainted?.find(t => t.lval === hovered); + const hoveredNode = hovered && uniqueRange(hovered, ranges); + if (!hoveredTaint || !hoveredNode) return undefined; + return { + pos: hoveredNode.from, + above: true, + strictSide: true, + arrow: true, + create: () => { + const dom = document.createElement('div'); + dom.className = 'cm-tainted-tooltip'; + dom.textContent = textOfTaint(hoveredTaint.taint); + return { dom }; + } + }; + }); +} + +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// Server requests +// ----------------------------------------------------------------------------- + +// Server request handler returning the given function's text. +function useFctText(fct: Fct): text { + return States.useRequest(Ast.printFunction, fct) ?? null; +} + +// Server request handler returning the given function's dead code information. +function useFctDead(fct: Fct): Eva.deadCode { + const empty = { unreachable: [], nonTerminating: [] }; + return States.useRequest(Eva.getDeadCode, fct) ?? empty; +} + +// Server request handler returning the given function's callers. +function useFctCallers(fct: Fct): Eva.CallSite[] { + return States.useRequest(Eva.getCallers, fct) ?? []; +} + +// Server request handler returning the tainted lvalues. +function useFctTaints(fct: Fct): Eva.LvalueTaints[] { + return States.useRequest(Eva.taintedLvalues, fct, { onError: [] }) ?? []; +} + +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// AST View component +// ----------------------------------------------------------------------------- + +// Necessary extensions for our needs. +const extensions: Editor.Extension[] = [ + MarkerUpdater, + MarkerScroller, + HoveredUpdater, + CodeDecorator, + DeadCodeDecorator, + ContextMenuHandler, + PropertiesGutter, + TaintedLvaluesDecorator, + TaintTooltip, + Editor.FoldGutter, + Editor.LanguageHighlighter, +]; + +// The component in itself. +export default function ASTview(): JSX.Element { + const [fontSize] = Settings.useGlobalSettings(Preferences.EditorFontSize); + const { view, Component } = Editor.Editor(extensions); + + // Updating CodeMirror when the selection or its callback are changed. + const [selection, setSel] = States.useSelection(); + React.useEffect(() => UpdateSelection.set(view, setSel), [view, setSel]); + const fct = selection?.current?.fct; + React.useEffect(() => Fct.set(view, fct), [view, fct]); + const marker = selection?.current?.marker; + React.useEffect(() => Marker.set(view, marker), [view, marker]); + + // Updating CodeMirror when the <updateHovered> callback is changed. + const [hov, setHov] = States.useHovered(); + React.useEffect(() => UpdateHovered.set(view, setHov), [view, setHov]); + React.useEffect(() => Hovered.set(view, hov?.marker ?? ''), [view, hov]); + + // Updating CodeMirror when the <properties> synchronized array is changed. + const props = States.useSyncArray(Properties.status).getArray(); + React.useEffect(() => PropertiesStatuses.set(view, props), [view, props]); + + // Updating CodeMirror when the <propStatusTags> map is changed. + const tags = States.useTags(Properties.propStatusTags); + React.useEffect(() => Tags.set(view, tags), [view, tags]); + + // Updating CodeMirror when the <markersInfo> synchronized array is changed. + const info = States.useSyncArray(Ast.markerInfo); + const getData = React.useCallback((key) => info.getData(key), [info]); + React.useEffect(() => GetMarkerData.set(view, getData), [view, getData]); + + // Retrieving data on currently selected function and updating CodeMirror when + // they have changed. + const text = useFctText(fct); + React.useEffect(() => Text.set(view, text), [view, text]); + const dead = useFctDead(fct); + React.useEffect(() => Dead.set(view, dead), [view, dead]); + const callers = useFctCallers(fct); + React.useEffect(() => Callers.set(view, callers), [view, callers]); + const taints = useFctTaints(fct); + React.useEffect(() => TaintedLvalues.set(view, taints), [view, taints]); - // Component return ( <> <TitleBar> @@ -342,35 +675,22 @@ export default function ASTview(): JSX.Element { <IconButton icon='CHEVRON.CONTRACT' visible={true} - onClick={foldAll} + onClick={() => Editor.foldAll(view)} title='Collapse all multi-line ACSL properties' className="titlebar-thin-icon" /> <IconButton icon='CHEVRON.EXPAND' visible={true} - onClick={unfoldAll} + onClick={() => Editor.unfoldAll(view)} title='Expand all multi-line ACSL properties' className="titlebar-thin-icon" /> <Inset /> </TitleBar> - <Text - buffer={buffer} - mode="text/x-csrc" - fontSize={fontSize} - selection={theMarker} - onHover={onHover} - onSelection={onSelection} - onContextMenu={onContextMenu} - foldGutter={true} - foldOptions={foldOptions} - gutters={['bullet', 'CodeMirror-foldgutter']} - readOnly - /> + <Component style={{ fontSize: `${fontSize}px`}} /> </> ); - } -// -------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/kernel/SourceCode.tsx b/ivette/src/frama-c/kernel/SourceCode.tsx index fb932f8c0192ad9bfc62941c940f9c713da12dc8..1d06325a0be9f5a492d2c8da59ee91d9e52d24c9 100644 --- a/ivette/src/frama-c/kernel/SourceCode.tsx +++ b/ivette/src/frama-c/kernel/SourceCode.tsx @@ -20,195 +20,268 @@ /* */ /* ************************************************************************ */ -// -------------------------------------------------------------------------- -// --- Source Code -// -------------------------------------------------------------------------- - import React from 'react'; -import * as Server from 'frama-c/server'; -import * as States from 'frama-c/states'; +import * as Path from 'path'; import * as Dome from 'dome'; import * as System from 'dome/system'; -import { RichTextBuffer } from 'dome/text/buffers'; -import { Text } from 'dome/text/editors'; -import { TitleBar } from 'ivette'; -import * as Preferences from 'ivette/prefs'; -import { functions, markerInfo, getMarkerAt } from 'frama-c/kernel/api/ast'; -import { Code } from 'dome/controls/labels'; -import { Hfill } from 'dome/layout/boxes'; -import { IconButton } from 'dome/controls/buttons'; -import * as Path from 'path'; +import * as Boxes from 'dome/layout/boxes'; +import * as Editor from 'dome/text/editor'; +import * as Labels from 'dome/controls/labels'; import * as Settings from 'dome/data/settings'; +import * as Buttons from 'dome/controls/buttons'; + +import { Selection, Document } from 'dome/text/editor'; + +import * as Server from 'frama-c/server'; +import * as States from 'frama-c/states'; import * as Status from 'frama-c/kernel/Status'; +import * as Ast from 'frama-c/kernel/api/ast'; -import CodeMirror from 'codemirror/lib/codemirror'; -import 'codemirror/addon/selection/active-line'; -import 'codemirror/addon/dialog/dialog.css'; -import 'codemirror/addon/search/search'; -import 'codemirror/addon/search/searchcursor'; +import * as Ivette from 'ivette'; +import * as Preferences from 'ivette/prefs'; -// -------------------------------------------------------------------------- -// --- Pretty Printing (Browser Console) -// -------------------------------------------------------------------------- -const D = new Dome.Debug('Source Code'); -// -------------------------------------------------------------------------- -// --- Source Code Printer -// -------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// Utilitary types and functions +// ----------------------------------------------------------------------------- -// The SourceCode component, producing the GUI part showing the source code -// corresponding to the selected function. -export default function SourceCode(): JSX.Element { +// Recovering the cursor position as a line and a column. +interface Position { line: number, column: number } +function getCursorPosition(view: Editor.View): Position { + const pos = view?.state.selection.main; + if (!view || !pos) return { line: 1, column: 1 }; + const line = view.state.doc.lineAt(pos.from).number; + const column = (pos.goalColumn ?? 0) + 1; + return { line, column }; +} + +// Error messages. +function setError(text: string): void { + Status.setMessage({ text, kind: 'error' }); +} + +// Function launching the external editor at the currently selected position. +async function edit(file: string, pos: Position, cmd: string): Promise<void> { + if (file === '') return; + const args = cmd + .replace('%s', file) + .replace('%n', pos.line.toString()) + .replace('%c', pos.column.toString()) + .split(' '); + const prog = args.shift(); if (!prog) return; + const text = `An error has occured when opening the external editor ${prog}`; + System.spawn(prog, args).catch(() => setError(text)); +} + +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// Fields declarations +// ----------------------------------------------------------------------------- + +// The Ivette selection must be updated by the CodeMirror plugin. This field +// adds the callback in the CodeMirror internal state. +type UpdateSelection = (a: States.SelectionActions) => void; +const UpdateSelection = Editor.createField<UpdateSelection>(() => { return; }); + +// Those fields contain the source code and the file name. +const Source = Editor.createTextField<string>('', (s) => s); +const File = Editor.createField<string>(''); + +// This field contains the command use to start the external editor. +const Command = Editor.createField<string>(''); + +// These field contains a callback that returns the source of a location. +type GetSource = (loc: States.Location) => Ast.source | undefined; +const GetSource = Editor.createField<GetSource>(() => undefined); + +// We keep track of the cursor location (i.e the location that is under the +// current CodeMirror cursor) and the ivette location (i.e the locations as +// seen by the outside world). Keeping track of both of them together force +// their update in one dispatch, which prevents strange bugs. +interface Locations { cursor?: States.Location, ivette: States.Location } + +// The Locations field. When given Locations, it will update the cursor field if +// and only if the new cursor location is not undefined. It simplifies the +// update in the SourceCode component itself. +const Locations = createLocationsField(); +function createLocationsField(): Editor.Field<Locations> { + const noField = { cursor: undefined, ivette: {} }; + const field = Editor.createField<Locations>(noField); + const set: Editor.Set<Locations> = (view, toBe) => { + const hasBeen = field.get(view?.state); + const cursor = toBe.cursor ?? hasBeen.cursor; + field.set(view, { cursor, ivette: toBe.ivette }); + }; + return { ...field, set }; +} + +// ----------------------------------------------------------------------------- - // Hooks - const [buffer] = React.useState(() => new RichTextBuffer()); - const [selection, updateSelection] = States.useSelection(); - const theFunction = selection?.current?.fct; - const theMarker = selection?.current?.marker; - const markersInfo = States.useSyncArray(markerInfo); - const functionsData = States.useSyncArray(functions).getArray(); - - // Retrieving the file name and the line number from the selection and the - // synchronized tables. - const sloc = - (theMarker && markersInfo.getData(theMarker)?.sloc) ?? - (theFunction && functionsData.find((e) => e.name === theFunction)?.sloc); - const file = sloc ? sloc.file : ''; - const line = sloc ? sloc.line : 0; - const filename = Path.parse(file).base; - // Global Font Size - const [fontSize] = Settings.useGlobalSettings(Preferences.EditorFontSize); - // Updating the buffer content. - const text = React.useMemo(async () => { - const onError = (): string => { - if (file) - D.error(`Fail to load source code file ${file}`); - return ''; - }; - return System.readFile(file).catch(onError); - }, [file]); - const { result } = Dome.usePromise(text); - React.useEffect(() => buffer.setValue(result), [buffer, result]); - - /* Last location selected by a click in the source code. */ - const selected: React.MutableRefObject<undefined | States.Location> = - React.useRef(); - - /* Updates the cursor position according to the current [selection], except - when the [selection] is changed according to a click in the source code, - in which case the cursor should stay exactly where the user clicked. */ - React.useEffect(() => { - if (selected.current && selected?.current === selection?.current) - selected.current = undefined; - else - buffer.setCursorOnTop(line); - }, [buffer, selection, line, result]); - - /* CodeMirror types used to bind callbacks to extraKeys. */ - type position = CodeMirror.Position; - type editor = CodeMirror.Editor; - - const selectCallback = React.useCallback( - async function select(editor: editor, event: MouseEvent) { - const pos = editor.coordsChar({ left: event.x, top: event.y }); - if (file === '' || !pos) return; - const arg = [file, pos.line + 1, pos.ch + 1]; - Server - .send(getMarkerAt, arg) - .then(([fct, marker]) => { - if (fct || marker) { - const location = { fct, marker } as States.Location; - selected.current = location; - updateSelection({ location }); - } - }) - .catch((err) => { - D.error(`Failed to get marker from source file position: ${err}`); - Status.setMessage({ - text: 'Failed request to Frama-C server', - kind: 'error', - }); - }); +// ----------------------------------------------------------------------------- +// Synchronisation with the outside world +// ----------------------------------------------------------------------------- + +// Update the outside world when the user click somewhere in the source code. +const SyncOnUserSelection = createSyncOnUserSelection(); +function createSyncOnUserSelection(): Editor.Extension { + const actions = { cmd: Command, update: UpdateSelection }; + const deps = { file: File, selection: Selection, doc: Document, ...actions }; + return Editor.createEventHandler(deps, { + mouseup: async ({ file, cmd, update }, view, event) => { + if (!view || file === '') return; + const pos = getCursorPosition(view); + const cursor = [file, pos.line, pos.column]; + try { + const [fct, marker] = await Server.send(Ast.getMarkerAt, cursor); + if (fct || marker) { + // The forced reload should NOT be necessary but... It is... + await Server.send(Ast.reloadMarkerInfo, null); + const location = { fct, marker }; + update({ location }); + Locations.set(view, { cursor: location, ivette: location }); + } + } catch { + setError('Failure'); + } + if (event.ctrlKey) edit(file, pos, cmd); + } + }); +} + +// Update the cursor position when the outside world changes the selected +// location. +const SyncOnOutsideSelection = createSyncOnOutsideSelection(); +function createSyncOnOutsideSelection(): Editor.Extension { + const deps = { locations: Locations, get: GetSource }; + return Editor.createViewUpdater(deps, ({ locations, get }, view) => { + const { cursor, ivette } = locations; + if (ivette === undefined || ivette === cursor) return; + const source = get(ivette); if (!source) return; + const newFct = ivette.fct !== cursor?.fct && ivette.marker === undefined; + const onTop = cursor === undefined || newFct; + Locations.set(view, { cursor: ivette, ivette }); + Editor.selectLine(view, source.line, onTop); + }); +} + +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// Context menu +// ----------------------------------------------------------------------------- + +// This events handler takes care of the context menu. +const ContextMenu = createContextMenu(); +function createContextMenu(): Editor.Extension { + const deps = { file: File, command: Command, update: UpdateSelection }; + return Editor.createEventHandler(deps, { + contextmenu: ({ file, command }, view) => { + if (file === '') return; + const label = 'Open file in an external editor'; + const pos = getCursorPosition(view); + Dome.popupMenu([ { label, onClick: () => edit(file, pos, command) } ]); }, - [file, updateSelection], - ); + }); +} - React.useEffect(() => { - buffer.forEach((cm) => cm.on('mousedown', selectCallback)); - return () => buffer.forEach((cm) => cm.off('mousedown', selectCallback)); - }, [buffer, selectCallback]); +// ----------------------------------------------------------------------------- + + +// ----------------------------------------------------------------------------- +// Server requests +// ----------------------------------------------------------------------------- + +// Server request handler returning the source code. +function useFctSource(file: string): string { + const req = React.useMemo(() => System.readFile(file), [file]); + const { result } = Dome.usePromise(req); + return result ?? ''; +} + +// Build a callback that retrieves a location's source information. +function useSourceGetter(): GetSource { + const markersInfo = States.useSyncArray(Ast.markerInfo); + const functionsData = States.useSyncArray(Ast.functions).getArray(); + return React.useCallback(({ fct, marker }) => { + const markerSloc = (marker !== undefined && marker !== '') ? + markersInfo.getData(marker)?.sloc : undefined; + const fctSloc = (fct !== undefined && fct !== '') ? + functionsData.find((e) => e.name === fct)?.sloc : undefined; + return markerSloc ?? fctSloc; + }, [markersInfo, functionsData]); +} + +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// Source Code component +// ----------------------------------------------------------------------------- + +// Necessary extensions. +const extensions: Editor.Extension[] = [ + Source, + Editor.Selection, + Editor.LineNumbers, + Editor.LanguageHighlighter, + Editor.HighlightActiveLine, + SyncOnUserSelection, + SyncOnOutsideSelection, + ContextMenu, + Locations.structure, +]; + +// The component in itself. +export default function SourceCode(): JSX.Element { + const [fontSize] = Settings.useGlobalSettings(Preferences.EditorFontSize); const [command] = Settings.useGlobalSettings(Preferences.EditorCommand); - async function launchEditor(_?: editor, pos?: position): Promise<void> { - if (file !== '') { - const selectedLine = pos ? (pos.line + 1).toString() : '1'; - const selectedChar = pos ? (pos.ch + 1).toString() : '1'; - const cmd = command - .replace('%s', file) - .replace('%n', selectedLine) - .replace('%c', selectedChar); - const args = cmd.split(' '); - const prog = args.shift(); - if (prog) System.spawn(prog, args).catch(() => { - Status.setMessage({ - text: `An error has occured when opening the external editor ${prog}`, - kind: 'error', - }); - }); - } - } - - async function contextMenu(editor?: editor, pos?: position): Promise<void> { - if (file !== '') { - const items = [ - { - label: 'Open file in an external editor', - onClick: () => launchEditor(editor, pos), - }, - ]; - Dome.popupMenu(items); - } - } + const { view, Component } = Editor.Editor(extensions); + const [selection, update] = States.useSelection(); + const loc = React.useMemo(() => selection?.current ?? {}, [selection]); + const getSource = useSourceGetter(); + const file = getSource(loc)?.file ?? ''; + const filename = Path.parse(file).base; + const pos = getCursorPosition(view); + const source = useFctSource(file); + + React.useEffect(() => UpdateSelection.set(view, update), [view, update]); + React.useEffect(() => GetSource.set(view, getSource), [view, getSource]); + React.useEffect(() => File.set(view, file), [view, file]); + React.useEffect(() => Source.set(view, source), [view, source]); + React.useEffect(() => Command.set(view, command), [view, command]); + React.useEffect(() => Locations.set(view, { ivette: loc }), [view, loc]); const externalEditorTitle = 'Open the source file in an external editor.\nA Ctrl-click ' + 'in the source code opens the editor at the selected location.' + '\nThe editor used can be configured in Ivette settings.'; - // Building the React component. return ( <> - <TitleBar> - <IconButton + <Ivette.TitleBar> + <Buttons.IconButton icon="DUPLICATE" visible={file !== ''} - onClick={launchEditor} + onClick={() => edit(file, pos, command)} title={externalEditorTitle} /> - <Code title={file}>{filename}</Code> - <Hfill /> - </TitleBar> - <Text - buffer={buffer} - mode="text/x-csrc" - fontSize={fontSize} - selection={theMarker} - lineNumbers={!!theFunction} - styleActiveLine={!!theFunction} - extraKeys={{ - 'Alt-F': 'findPersistent', - 'Ctrl-LeftClick': launchEditor as (_: CodeMirror.Editor) => void, - RightClick: contextMenu as (_: CodeMirror.Editor) => void, - }} - readOnly - /> + <Labels.Code title={file}>{filename}</Labels.Code> + <Boxes.Hfill /> + </Ivette.TitleBar> + <Component style={{ fontSize: `${fontSize}px` }} /> </> ); - } -// -------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/plugins/eva/api/general/index.ts b/ivette/src/frama-c/plugins/eva/api/general/index.ts index 7bb38314aadfb773fab25c3cdf94d51abc488ffb..85c3985681b3c6821a2e683ada03a648b4a860a8 100644 --- a/ivette/src/frama-c/plugins/eva/api/general/index.ts +++ b/ivette/src/frama-c/plugins/eva/api/general/index.ts @@ -94,25 +94,38 @@ const computationState_internal: State.Value<computationStateType> = { /** The current computation state of the analysis. */ export const computationState: State.Value<computationStateType> = computationState_internal; -const getCallers_internal: Server.GetRequest< - Json.key<'#fct'>, - [ Json.key<'#fct'>, Json.key<'#stmt'> ][] - > = { +/** CallSite */ +export interface CallSite { + /** Function */ + kf: Json.key<'#fct'>; + /** Statement */ + stmt: Json.key<'#stmt'>; +} + +/** Decoder for `CallSite` */ +export const jCallSite: Json.Decoder<CallSite> = + Json.jObject({ + kf: Json.jKey<'#fct'>('#fct'), + stmt: Json.jKey<'#stmt'>('#stmt'), + }); + +/** Natural order for `CallSite` */ +export const byCallSite: Compare.Order<CallSite> = + Compare.byFields + <{ kf: Json.key<'#fct'>, stmt: Json.key<'#stmt'> }>({ + kf: Compare.string, + stmt: Compare.string, + }); + +const getCallers_internal: Server.GetRequest<Json.key<'#fct'>,CallSite[]> = { kind: Server.RqKind.GET, name: 'plugins.eva.general.getCallers', input: Json.jKey<'#fct'>('#fct'), - output: Json.jArray( - Json.jPair( - Json.jKey<'#fct'>('#fct'), - Json.jKey<'#stmt'>('#stmt'), - )), + output: Json.jArray(jCallSite), signals: [], }; /** Get the list of call site of a function */ -export const getCallers: Server.GetRequest< - Json.key<'#fct'>, - [ Json.key<'#fct'>, Json.key<'#stmt'> ][] - >= getCallers_internal; +export const getCallers: Server.GetRequest<Json.key<'#fct'>,CallSite[]>= getCallers_internal; /** Data for array rows [`functions`](#functions) */ export interface functionsData { @@ -257,6 +270,42 @@ const taintStatusTags_internal: Server.GetRequest<null,tag[]> = { /** Registered tags for the above type. */ export const taintStatusTags: Server.GetRequest<null,tag[]>= taintStatusTags_internal; +/** Lvalue taint status */ +export interface LvalueTaints { + /** tainted lvalue */ + lval: Json.key<'#lval'>; + /** taint status */ + taint: taintStatus; +} + +/** Decoder for `LvalueTaints` */ +export const jLvalueTaints: Json.Decoder<LvalueTaints> = + Json.jObject({ lval: Json.jKey<'#lval'>('#lval'), taint: jTaintStatus,}); + +/** Natural order for `LvalueTaints` */ +export const byLvalueTaints: Compare.Order<LvalueTaints> = + Compare.byFields + <{ lval: Json.key<'#lval'>, taint: taintStatus }>({ + lval: Compare.string, + taint: byTaintStatus, + }); + +const taintedLvalues_internal: Server.GetRequest< + Json.key<'#fundec'>, + LvalueTaints[] + > = { + kind: Server.RqKind.GET, + name: 'plugins.eva.general.taintedLvalues', + input: Json.jKey<'#fundec'>('#fundec'), + output: Json.jArray(jLvalueTaints), + signals: [], +}; +/** Get the tainted lvalues of a given function */ +export const taintedLvalues: Server.GetRequest< + Json.key<'#fundec'>, + LvalueTaints[] + >= taintedLvalues_internal; + /** Data for array rows [`properties`](#properties) */ export interface propertiesData { /** Entry identifier. */ diff --git a/ivette/yarn.lock b/ivette/yarn.lock index 68c29c52e021ed01caa40b93413da412b2c14ff5..aa98dbadf52cd4824088c375fd06493f2cb58f29 100644 --- a/ivette/yarn.lock +++ b/ivette/yarn.lock @@ -1786,6 +1786,50 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@codemirror/commands@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.1.0.tgz#c9da851f419f25dae400d7cd94f80b80ef060696" + integrity sha512-qCj2YqmbBjj0P1iumnlL5lBqZvJPzT+t2UvgjcaXErp5ZvMqFRVgQyrEfdXX6SX5UcvcHKBjXqno+MkUp0aYvQ== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + "@lezer/common" "^1.0.0" + +"@codemirror/lang-cpp@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@codemirror/lang-cpp/-/lang-cpp-6.0.1.tgz#7e91d193cedc3cac5135c04b1aac881d957b2c8a" + integrity sha512-46p3ohfhjzkLWJ3VwvzX0aqlXh8UkEqX1xo2Eds9l6Ql3uDoxI2IZEjR9cgJaGOZTXCkDzQuQH7sfYAxMvzLjA== + dependencies: + "@codemirror/language" "^6.0.0" + "@lezer/cpp" "^1.0.0" + +"@codemirror/language@6.2.1", "@codemirror/language@^6.0.0": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.2.1.tgz#cb10cd785a76e50ecd2fe2dc59ff66af8a41b87a" + integrity sha512-MC3svxuvIj0MRpFlGHxLS6vPyIdbTr2KKPEW46kCoCXw2ktb4NTkpkPBI/lSP/FoNXLCBJ0mrnUi1OoZxtpW1Q== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + "@lezer/common" "^1.0.0" + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + style-mod "^4.0.0" + +"@codemirror/state@6.1.1", "@codemirror/state@^6.0.0": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.1.1.tgz#4f512e5e34ea23a5e10b2c1fe43f6195e90417bb" + integrity sha512-2s+aXsxmAwnR3Rd+JDHPG/1lw0YsA9PEwl7Re88gHJHGfxyfEzKBmsN4rr53RyPIR4lzbbhJX0DCq0WlqlBIRw== + +"@codemirror/view@6.2.3", "@codemirror/view@^6.0.0": + version "6.2.3" + resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.2.3.tgz#ba6e70037b1deee5e2d9e94541305aa50ede4715" + integrity sha512-cgN9gWS9+kv9+eOgVJWMrGUk4EwYKBZpuFYvxIlu4CmMye3+U+gMzuZhBgtPDOCbCp30hxFIOO0MENhGfnaC/g== + dependencies: + "@codemirror/state" "^6.0.0" + style-mod "^4.0.0" + w3c-keyname "^2.2.4" + "@develar/schema-utils@~2.6.5": version "2.6.5" resolved "https://registry.yarnpkg.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz#3ece22c5838402419a6e0425f85742b961d9b6c6" @@ -1883,6 +1927,33 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@lezer/common@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.0.1.tgz#d014fda6d582c24336fadf2715e76f02f73c8908" + integrity sha512-8TR5++Q/F//tpDsLd5zkrvEX5xxeemafEaek7mUp7Y+bI8cKQXdSqhzTOBaOogETcMOVr0pT3BBPXp13477ciw== + +"@lezer/cpp@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@lezer/cpp/-/cpp-1.0.0.tgz#3293fd88aaf16a6d4f18188602b4d931be8f0915" + integrity sha512-Klk3/AIEKoptmm6cNm7xTulNXjdTKkD+hVOEcz/NeRg8tIestP5hsGHJeFDR/XtyDTxsjoPjKZRIGohht7zbKw== + dependencies: + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + +"@lezer/highlight@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.0.0.tgz#1dc82300f5d39fbd67ae1194b5519b4c381878d3" + integrity sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA== + dependencies: + "@lezer/common" "^1.0.0" + +"@lezer/lr@^1.0.0": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.2.3.tgz#f44ca844f15f6762fde4eab877d110567e34ffa1" + integrity sha512-qpB7rBzH8f6Mzjv2AVZRahcm+2Cf7nbIH++uXbvVOL1yIRvVWQ3HAM/saeBLCyz/togB7LGo76qdJYL1uKQlqA== + dependencies: + "@lezer/common" "^1.0.0" + "@malept/cross-spawn-promise@^1.1.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d" @@ -9152,6 +9223,11 @@ style-loader@^1.1.3: loader-utils "^2.0.0" schema-utils "^2.7.0" +style-mod@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.0.0.tgz#97e7c2d68b592975f2ca7a63d0dd6fcacfe35a01" + integrity sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw== + sumchecker@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42" @@ -9706,6 +9782,11 @@ vscode-textmate@5.2.0: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== +w3c-keyname@^2.2.4: + version "2.2.6" + resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.6.tgz#8412046116bc16c5d73d4e612053ea10a189c85f" + integrity sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg== + watchpack-chokidar2@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957" diff --git a/src/plugins/eva/api/general_requests.ml b/src/plugins/eva/api/general_requests.ml index 2d185dc0db970e9d89e1df9c6e1bc8234568e2d5..216313063866d220a8376240db8ee9fdf02153a7 100644 --- a/src/plugins/eva/api/general_requests.ml +++ b/src/plugins/eva/api/general_requests.ml @@ -57,17 +57,39 @@ let _computation_signal = ~add_hook:Analysis.register_computation_hook () -module CallSite = Data.Jpair (Kernel_ast.Kf) (Kernel_ast.Stmt) -let callers kf = - let list = Results.callsites kf in - List.concat (List.map (fun (kf, l) -> List.map (fun s -> kf, s) l) list) -let () = Request.register ~package - ~kind:`GET ~name:"getCallers" - ~descr:(Markdown.plain "Get the list of call site of a function") - ~input:(module Kernel_ast.Kf) ~output:(module Data.Jlist (CallSite)) - callers +(* ----- Callsites ---------------------------------------------------------- *) + +module CallSite = struct + open Data + type callsite + let record: callsite Record.signature = Record.signature () + + let kf_field = Record.field record ~name:"kf" + ~descr:(Markdown.plain "Function") (module Kernel_ast.Kf) + + let stmt_field = Record.field record ~name:"stmt" + ~descr:(Markdown.plain "Statement") (module Kernel_ast.Stmt) + + let data = Record.publish ~package ~name:"CallSite" + ~descr:(Markdown.plain "CallSite") record + + module R : Record.S with type r = callsite = (val data) + + let convert (kf, stmts) = stmts |> List.map @@ fun stmt -> + R.default |> R.set kf_field kf |> R.set stmt_field stmt + + let callers kf = Results.callsites kf |> List.map convert |> List.concat + + let () = Request.register ~package + ~kind:`GET ~name:"getCallers" + ~descr:(Markdown.plain "Get the list of call site of a function") + ~input:(module Kernel_ast.Kf) ~output:(module Data.Jlist (R)) + callers +end + + (* ----- Functions ---------------------------------------------------------- *) @@ -93,6 +115,8 @@ struct Analysis.register_computation_hook (fun _ -> f () )) end + + (* ----- Dead code: unreachable and non-terminating statements -------------- *) type dead_code = @@ -109,6 +133,7 @@ module DeadCode = struct let unreachable = Record.field record ~name:"unreachable" ~descr:(Markdown.plain "List of unreachable statements.") (module Data.Jlist (Kernel_ast.Marker)) + let non_terminating = Record.field record ~name:"nonTerminating" ~descr:(Markdown.plain "List of reachable but non terminating statements.") (module Data.Jlist (Kernel_ast.Marker)) @@ -159,7 +184,9 @@ let () = Request.register ~package ~output:(module DeadCode) dead_code -(* ----- Register Eva information ------------------------------------------- *) + + +(* ----- Register Eva values information ------------------------------------ *) let term_lval_to_lval tlval = try Logic_to_c.term_lval_to_lval tlval @@ -199,64 +226,79 @@ let () = ~enable:Analysis.is_computed print_value -let print_taint fmt marker = - let loc = Cil_datatype.Location.unknown in - let expr, stmt = - match marker with - | Printer_tag.PLval (_kf, Kstmt stmt, lval) -> - Cil.new_exp ~loc (Lval lval), stmt - | Printer_tag.PExp (_kf, Kstmt stmt, expr) -> expr, stmt - | PVDecl (_kf, Kstmt stmt, vi) -> - Cil.new_exp ~loc (Lval (Var vi, NoOffset)), stmt - | PTermLval (_kf, Kstmt stmt, _ip, tlval) -> - let lval = term_lval_to_lval tlval in - Cil.new_exp ~loc (Lval lval), stmt - | _ -> raise Not_found - in - let evaluate_taint request = - let deps = Results.expr_dependencies expr request in - Result.get_ok (Results.is_tainted deps.data request), - Result.get_ok (Results.is_tainted deps.indirect request) - in - let before = evaluate_taint Results.(before stmt) in - let after = evaluate_taint Results.(after stmt) in - let str_taint = function - | Results.Untainted -> "untainted" + + +(* ----- Register Eva taints information ------------------------------------ *) + +let expr_of_lval v = Cil.new_exp ~loc:Cil_datatype.Location.unknown (Lval v) + +module EvaTaints = struct + open Results + + let evaluate expr request = + let (let+) = Option.bind in + let { data ; indirect } = expr_dependencies expr request in + let+ data = is_tainted data request |> Result.to_option in + let+ indirect = is_tainted indirect request |> Result.to_option in + Some (data, indirect) + + let expr_of_marker = let open Printer_tag in function + | PLval (_, Kstmt stmt, lval) -> Some (expr_of_lval lval, stmt) + | PExp (_, Kstmt stmt, expr) -> Some (expr, stmt) + | PVDecl (_, Kstmt stmt, vi) -> Some (expr_of_lval (Var vi, NoOffset), stmt) + | PTermLval (_, Kstmt stmt, _, tlval) -> + Some (term_lval_to_lval tlval |> expr_of_lval, stmt) + | _ -> None + + let of_marker marker = + let (let+) = Option.bind in + let+ expr, stmt = expr_of_marker marker in + let+ before = evaluate expr (before stmt) in + let+ after = evaluate expr (after stmt) in + Some (before, after) + + let to_string = function + | Untainted -> "untainted" | Direct -> "direct taint" | Indirect -> "indirect taint" - in - let pretty fmt = let open Results in function - | taint, Untainted -> Format.fprintf fmt "%s" (str_taint taint) - | t1, t2 -> - Format.fprintf fmt - "%s to the value, %s %s to values used to compute lvalues addresses" - (str_taint t1) (if t1 = Untainted then "but" else "and") (str_taint t2) - in - if before = after - then Format.fprintf fmt "%a" pretty before - else Format.fprintf fmt "Before: %a@\nAfter: %a" pretty before pretty after -let () = - let enable () = Analysis.is_computed () && Taint_domain.Store.is_computed () in - let title = + let pretty fmt = function + | taint, Untainted -> Format.fprintf fmt "%s" (to_string taint) + | data, indirect -> + let sep = match data with Untainted -> "but" | _ -> "and" in + Format.fprintf fmt + "%s to the value, %s %s to values used to compute lvalues adresses" + (to_string data) sep (to_string indirect) + + let print_taint fmt marker = + let before, after = of_marker marker |> Option.get in + if before = after then Format.fprintf fmt "%a" pretty before + else Format.fprintf fmt "Before: %a@\nAfter: %a" pretty before pretty after + + let eva_taints_title = "Taint status:\n\ - Direct taint: data dependency from values provided by the attacker, \ meaning that the attacker may be able to alter this value\n\ - Indirect taint: the attacker cannot directly alter this value, but he \ may be able to impact the path by which its value is computed.\n\ - Untainted: cannot be modified by the attacker." - in - Server.Kernel_ast.Information.register - ~id:"eva.taint" ~label:"Taint" ~descr: "Taint status according to Eva" - ~title ~enable print_taint -let () = - Analysis.register_computation_hook - (fun _ -> Server.Kernel_ast.Information.update ()) + let () = + let taint_computed = Taint_domain.Store.is_computed in + let enable () = Analysis.is_computed () && taint_computed () in + Server.Kernel_ast.Information.register + ~id:"eva.taint" ~label:"Taint" ~descr: "Taint status according to Eva" + ~title:eva_taints_title ~enable print_taint -(* ----- Red and tainted alarms --------------------------------------------- *) + let update = Server.Kernel_ast.Information.update + let () = Analysis.register_computation_hook (fun _ -> update ()) +end + + + +(* ----- Taint statuses ----------------------------------------------------- *) -module Taint = struct +module TaintStatus = struct open Server.Data type taint = Results.taint = Direct | Indirect | Untainted @@ -271,49 +313,98 @@ module Taint = struct ~value dictionary let tag_not_computed = - tag (Error NotComputed) "not_computed" "" - "Not computed" "the Eva taint domain has not been enabled, \ - or the Eva analysis has not been run" + tag (Error NotComputed) "not_computed" "" "Not computed" + "the Eva taint domain has not been enabled, \ + or the Eva analysis has not been run" let tag_error = - tag (Error LogicError) "error" "Error" - "Error" "the memory zone on which this property depends \ - could not be computed" + tag (Error LogicError) "error" "Error" "Error" + "the memory zone on which this property depends could not be computed" let tag_not_applicable = - tag (Error Irrelevant) "not_applicable" "—" - "Not applicable" "no taint for this kind of property" + tag (Error Irrelevant) "not_applicable" "—" "Not applicable" + "no taint for this kind of property" let tag_direct_taint = - tag (Ok Direct) "direct_taint" "Tainted (direct)" - "Direct taint" + tag (Ok Direct) "direct_taint" "Tainted (direct)" "Direct taint" "this property is related to a memory location that can be affected \ by an attacker" let tag_indirect_taint = - tag (Ok Indirect) "indirect_taint" "Tainted (indirect)" - "Indirect taint" + tag (Ok Indirect) "indirect_taint" "Tainted (indirect)" "Indirect taint" "this property is related to a memory location whose assignment depends \ on path conditions that can be affected by an attacker" let tag_untainted = - tag (Ok Untainted) "not_tainted" "Untainted" - "Untainted property" "this property is safe" - - let () = Enum.set_lookup dictionary - begin function - | Error NotComputed -> tag_not_computed - | Error Irrelevant -> tag_not_applicable - | Error LogicError -> tag_error - | Ok Direct -> tag_direct_taint - | Ok Indirect -> tag_indirect_taint - | Ok Untainted -> tag_untainted - end + tag (Ok Untainted) "not_tainted" "Untainted" "Untainted property" + "this property is safe" + + let () = Enum.set_lookup dictionary @@ function + | Error NotComputed -> tag_not_computed + | Error Irrelevant -> tag_not_applicable + | Error LogicError -> tag_error + | Ok Direct -> tag_direct_taint + | Ok Indirect -> tag_indirect_taint + | Ok Untainted -> tag_untainted let data = Request.dictionary ~package ~name:"taintStatus" ~descr:(Markdown.plain "Taint status of logical properties") dictionary include (val data : S with type t = (taint, error) result) +end + + + +(* ----- Tainted lvalues ---------------------------------------------------- *) + +module LvalueTaints = struct + module Table = Cil_datatype.Lval.Hashtbl + + module Status = struct + type record + let record: record Data.Record.signature = Data.Record.signature () + let field name d = Data.Record.field record ~name ~descr:(Markdown.plain d) + let lval_field = field "lval" "tainted lvalue" (module Kernel_ast.Lval) + let taint_field = field "taint" "taint status" (module TaintStatus) + let name, descr = "LvalueTaints", Markdown.plain "Lvalue taint status" + let publication = Data.Record.publish record ~package ~name ~descr + include (val publication: Data.Record.S with type r = record) + let create lval taint = set lval_field lval @@ set taint_field taint default + end + + let current_project () = Visitor_behavior.inplace () + class tainted_lvalues taints = object (self) + inherit Visitor.generic_frama_c_visitor (current_project ()) + method! vlval lval = + let expr = expr_of_lval lval in + match self#current_stmt with + | None -> DoChildren + | Some stmt -> + match Results.after stmt |> EvaTaints.evaluate expr with + | Some (Results.Untainted, _) -> DoChildren + | Some (t, _) -> Table.add taints lval (Kstmt stmt, t) ; DoChildren + | None -> DoChildren + end + + let get_tainted_lvals fundec = + let taints = Table.create 17 in + Visitor.visitFramacFunction (new tainted_lvalues taints) fundec |> ignore ; + let fn lval (ki, taint) acc = Status.create (ki, lval) (Ok taint) :: acc in + Table.fold fn taints [] |> List.rev + + let () = Request.register ~package ~kind:`GET ~name:"taintedLvalues" + ~descr:(Markdown.plain "Get the tainted lvalues of a given function") + ~input:(module (Kernel_ast.Fundec)) + ~output:(module (Data.Jlist (Status))) + get_tainted_lvals +end + + + +(* ----- Red and tainted alarms --------------------------------------------- *) + +module PropertiesData = struct + open TaintStatus let zone_of_predicate kinstr predicate = let state = Results.(before_kinstr kinstr |> get_cvalue_model) in @@ -336,40 +427,39 @@ module Taint = struct | _ -> Error Irrelevant let is_tainted_property ip = - if not (Analysis.is_computed () && Taint_domain.Store.is_computed ()) - then Error NotComputed - else + if Analysis.is_computed () && Taint_domain.Store.is_computed () then let (let+) = Result.bind in let kinstr = Property.get_kinstr ip in let+ predicate = get_predicate ip in let+ zone = zone_of_predicate kinstr predicate in let result = Results.(before_kinstr kinstr |> is_tainted zone) in Result.map_error (fun _ -> NotComputed) result -end + else Error NotComputed -let model = States.model () - -let () = States.column model ~name:"priority" - ~descr:(Markdown.plain "Is the property invalid in some context \ - of the analysis?") - ~data:(module Data.Jbool) - ~get:(fun ip -> Red_statuses.is_red ip) - -let () = States.column model ~name:"taint" - ~descr:(Markdown.plain "Is the property tainted according to \ - the Eva taint domain?") - ~data:(module Taint) - ~get:(fun ip -> Taint.is_tainted_property ip) + let () = + let model = States.model () in + let descr = "Is the property invalid in some context of the analysis?" in + States.column model + ~name:"priority" + ~descr:(Markdown.plain descr) + ~data:(module Data.Jbool) + ~get:Red_statuses.is_red ; + let descr = "Is the property tainted according to the Eva taint domain?" in + States.column model + ~name:"taint" + ~descr:(Markdown.plain descr) + ~data:(module TaintStatus) + ~get:is_tainted_property ; + ignore @@ States.register_array + ~package + ~name:"properties" + ~descr:(Markdown.plain "Status of Registered Properties") + ~key:(fun ip -> Kernel_ast.Marker.create (PIP ip)) + ~keyType:Kernel_ast.Marker.jproperty + ~iter:Property_status.iter + model +end -let _array = - States.register_array - ~package - ~name:"properties" - ~descr:(Markdown.plain "Status of Registered Properties") - ~key:(fun ip -> Kernel_ast.Marker.create (PIP ip)) - ~keyType:Kernel_ast.Marker.jproperty - ~iter:Property_status.iter - model (* ----- Analysis statistics ------------------------------------------------ *) @@ -581,6 +671,7 @@ let _array = model + (* ----- Domains states ----------------------------------------------------- *) let compute_lval_deps request lval =