Class: RemLint::Trigger

Inherits:
Object
  • Object
show all
Defined in:
lib/remlint/trigger.rb

Overview

The clauses of one command's trigger, and where its body starts.

Everything before the reminder type -- MSG, MSF, RUN, CAL, SATISFY, SPECIAL, PS, PSFILE -- is the trigger. Everything after is the body, and the body is text.

That boundary is the whole point of this class. REM Tue AT 15:00 MSG Meet Bob at the pub contains the word at twice: once as the clause that sets the time, once as an ordinary English preposition. A rule that grepped the line for AT would find both. Scanning stops at the body keyword, so only the first one is a clause.

Hyphenated clause keywords (MAX-OVERDUE, COMPLETE-THROUGH) lex as three tokens, so a name followed by a hyphen and another name is retried as one word before being taken as two.

Defined Under Namespace

Classes: Clause

Constant Summary collapse

TRIGGER_COMMANDS =

The commands that open a trigger. Everything else -- ERRMSG, SET, BANNER, INCLUDE -- takes its own argument, and scanning that argument for a reminder type finds words rather than keywords. Remind's own tests/tstlang.rem has

errmsg Please run [filename()] with the -q and -r options

where run is English. Treating it as the start of a RUN body makes the rest of the sentence look like a shell command.

%w[REM OMIT IFTRIG].freeze
EXPRESSION_BODIES =

A body Remind runs the substitution filter over: everything but SATISFY.

SATISFY's body is an expression, and in an expression % is the modulo operator (expr.c:1473). SATISFY [($Ty % 4) == 0] in Remind's own examples/defs.rem is arithmetic, not a malformed substitution, and a rule that scanned it for % sequences would say so on every leap-year test ever written.

%w[SATISFY].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tokens, triggered: true) ⇒ Trigger

Returns a new instance of Trigger.



56
57
58
59
60
61
62
63
64
65
# File 'lib/remlint/trigger.rb', line 56

def initialize(tokens, triggered: true)
  @tokens = tokens
  @clauses = []
  @body = nil
  @triggered = triggered

  if triggered
    scan
  end
end

Instance Attribute Details

#bodyObject (readonly)

Returns the value of attribute body.



54
55
56
# File 'lib/remlint/trigger.rb', line 54

def body
  @body
end

#clausesObject (readonly)

Returns the value of attribute clauses.



54
55
56
# File 'lib/remlint/trigger.rb', line 54

def clauses
  @clauses
end

Class Method Details

.of(tokens, triggered: true) ⇒ Object



74
75
76
# File 'lib/remlint/trigger.rb', line 74

def self.of(tokens, triggered: true)
  new(tokens, triggered: triggered)
end

.triggered?(command) ⇒ Boolean

Whether this command carries a trigger at all: the three commands that open one, a bare reminder type (MSG hello), or a trigger written with the REM left off.

Returns:

  • (Boolean)


81
82
83
84
85
# File 'lib/remlint/trigger.rb', line 81

def self.triggered?(command)
  command.implicit? ||
    command.keyword?(*TRIGGER_COMMANDS) ||
    !!command.keyword&.body?
end

Instance Method Details

#body?Boolean

Returns:

  • (Boolean)


99
100
101
# File 'lib/remlint/trigger.rb', line 99

def body?
  !body.nil?
end

#body_offsetObject

Where the body's text begins in the logical line, or nil.



127
128
129
# File 'lib/remlint/trigger.rb', line 127

def body_offset
  body&.end_offset
end

#body_tokensObject

The tokens after the reminder type, which are the body's text. Empty for a command with no body keyword at all.



118
119
120
121
122
123
124
# File 'lib/remlint/trigger.rb', line 118

def body_tokens
  if body
    @tokens[(body.token_index + 1)..] || []
  else
    []
  end
end

#find(name) ⇒ Object

The clause of that name, or nil. Names are canonical, so find("UNTIL") matches an abbreviated UNTIL written as UNTI.



89
90
91
# File 'lib/remlint/trigger.rb', line 89

def find(name)
  clauses.find { |clause| clause.name == name.upcase }
end

#include?(*names) ⇒ Boolean

Returns:

  • (Boolean)


93
94
95
96
97
# File 'lib/remlint/trigger.rb', line 93

def include?(*names)
  wanted = names.map(&:upcase)

  clauses.any? { |clause| wanted.include?(clause.name) }
end

#text_body?Boolean

Returns:

  • (Boolean)


112
113
114
# File 'lib/remlint/trigger.rb', line 112

def text_body?
  body? && !EXPRESSION_BODIES.include?(body.name)
end

#triggered?Boolean

Whether this command carries a trigger at all. Rules that look for something positional -- a repeat, a delta -- need it, because SET a 3*14 has a * in it and no trigger for that * to be part of.

Returns:

  • (Boolean)


70
71
72
# File 'lib/remlint/trigger.rb', line 70

def triggered?
  @triggered
end